From: Zdenek Dohnal Date: Tue, 14 Apr 2026 16:37:25 +0000 (+0000) Subject: patch 9.2.0344: channel: ch_listen() can bind to network interface X-Git-Tag: v9.2.0344^0 X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=962a540d7653b0153b9250f8fafb1562bbc5211f;p=thirdparty%2Fvim.git patch 9.2.0344: channel: ch_listen() can bind to network interface Problem: channel: ch_listen() can bind to network interface Solution: Only allow to use Unix domain sockets or localhost interface (Zdenek Dohnal) related: #19231 related: #19799 closes: #19973 Signed-off-by: Zdenek Dohnal Signed-off-by: Christian Brabandt --- diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index eb802ed75e..f929b9e4c6 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -1,4 +1,4 @@ -*builtin.txt* For Vim version 9.2. Last change: 2026 Apr 10 +*builtin.txt* For Vim version 9.2. Last change: 2026 Apr 14 VIM REFERENCE MANUAL by Bram Moolenaar @@ -107,7 +107,8 @@ ch_getbufnr({handle}, {what}) Number get buffer number for {handle}/{what} ch_getjob({channel}) Job get the Job of {channel} ch_info({handle}) Dict info about channel {handle} ch_listen({address} [, {options}]) - Channel listen on {address} + Channel listen on {address} - port on loopback + or UNIX domain socket ch_log({msg} [, {handle}]) none write {msg} in the channel log file ch_logfile({fname} [, {mode}]) none start logging channel activity ch_open({address} [, {options}]) diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt index 55b95c632f..b8aaa92768 100644 --- a/runtime/doc/channel.txt +++ b/runtime/doc/channel.txt @@ -1,4 +1,4 @@ -*channel.txt* For Vim version 9.2. Last change: 2026 Apr 06 +*channel.txt* For Vim version 9.2. Last change: 2026 Apr 14 VIM REFERENCE MANUAL by Bram Moolenaar @@ -132,7 +132,7 @@ Start Vim and create a listening channel: > endfunc " Start listening on port 8765 - let server = ch_listen('localhost:8765', {"callback": "OnAccept"}) + let server = ch_listen('8765', {"callback": "OnAccept"}) From another Vim instance (or any program) you can connect to it: > let channel = ch_open('localhost:8765') @@ -637,8 +637,7 @@ ch_info({handle}) *ch_info()* "status" "open", "buffered" or "closed", like ch_status() When opened with ch_open(): - "hostname" the hostname of the address - "port" the port of the address + "port" the port on loopback "path" the path of the Unix-domain socket "sock_status" "open" or "closed" "sock_mode" "NL", "RAW", "JSON" or "JS" @@ -668,14 +667,15 @@ ch_info({handle}) *ch_info()* Return type: dict ch_listen({address} [, {options}]) *E1573* *E1574* *ch_listen()* - Listen on {address} for incoming channel connections. - This creates a server-side channel, unlike |ch_open()| - which connects to an existing server. + Listen on {address} - port on loopback or UNIX domain socket + for incoming channel connections. This creates a server-side + channel, unlike |ch_open()|which connects to an existing server. Returns a Channel. Use |ch_status()| to check for failure. {address} is a String, see |channel-address| for the possible - accepted forms, however binding to all interfaces is not - allowed for security reasons. + accepted forms, however in case of TCP sockets it allows to + set only a port and binds to loopback address for + security reasons. Note: IPv6 is not yet supported. If {options} is given it must be a |Dictionary|. diff --git a/src/channel.c b/src/channel.c index 6b4bd3cf2b..edf6d84c7a 100644 --- a/src/channel.c +++ b/src/channel.c @@ -1400,8 +1400,7 @@ theend: channel_T * channel_listen_func(typval_T *argvars) { - char_u *address; - char_u *p; + char_u *arg; char *rest; int port; int is_unix = FALSE; @@ -1409,62 +1408,31 @@ channel_listen_func(typval_T *argvars) channel_T *channel = NULL; if (in_vim9script() - && (check_for_string_arg(argvars, 0) == FAIL - || check_for_opt_dict_arg(argvars, 1) == FAIL)) - return NULL; - - address = tv_get_string(&argvars[0]); - if (argvars[1].v_type != VAR_UNKNOWN - && check_for_nonnull_dict_arg(argvars, 1) == FAIL) + && check_for_string_arg(argvars, 0) == FAIL) return NULL; - if (*address == NUL) + arg = tv_get_string(&argvars[0]); + if (*arg == NUL) { - semsg(_(e_invalid_argument_str), address); + semsg(_(e_invalid_argument_str), arg); return NULL; } - if (!STRNCMP(address, "unix:", 5)) + if (!STRNCMP(arg, "unix:", 5)) { is_unix = TRUE; - address += 5; + arg += 5; port = 0; } - else if (*address == '[') - { - // ipv6 address - p = vim_strchr(address + 1, ']'); - if (p == NULL || *++p != ':') - { - semsg(_(e_invalid_argument_str), address); - return NULL; - } - port = strtol((char *)(p + 1), &rest, 10); - if (port < 0 || port >= 65536 || *rest != NUL) - { - semsg(_(e_invalid_argument_str), address); - return NULL; - } - // strip '[' and ']' - ++address; - *(p - 1) = NUL; - } else { - // ipv4 address - p = vim_strchr(address, ':'); - if (p == NULL) - { - semsg(_(e_invalid_argument_str), address); - return NULL; - } - port = strtol((char *)(p + 1), &rest, 10); + port = strtol((char *)(arg), &rest, 10); if (port < 0 || port >= 65536 || *rest != NUL) { - semsg(_(e_invalid_argument_str), address); + semsg(_(e_invalid_argument_str), arg); return NULL; } - *p = NUL; + *arg = NUL; } // parse options @@ -1481,9 +1449,9 @@ channel_listen_func(typval_T *argvars) } if (is_unix) - channel = channel_listen_unix((char *)address, NULL); + channel = channel_listen_unix((char *)arg, NULL); else - channel = channel_listen((char *)address, port, NULL); + channel = channel_listen(port, NULL); if (channel != NULL) { opt.jo_set = JO_ALL; @@ -1501,7 +1469,6 @@ theend: */ channel_T * channel_listen( - char *hostname, int port_in, void (*nb_close_cb)(void)) { @@ -1513,12 +1480,6 @@ channel_listen( int val = 1; channel_T *channel; - if (hostname == NULL || *hostname == NUL) - { - ch_error(NULL, "Hostname/address not defined."); - return NULL; - } - #ifdef MSWIN channel_init_winsock(); #endif @@ -1535,42 +1496,7 @@ channel_listen( vim_memset((char *)&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(port_in); - -#ifdef FEAT_IPV6 - struct addrinfo hints; - struct addrinfo *res = NULL; - int err; - - CLEAR_FIELD(hints); - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_STREAM; - if ((err = getaddrinfo(hostname, NULL, &hints, &res)) != 0) - { - ch_error(channel, "in getaddrinfo() in channel_listen()"); - PERROR(_(e_gethostbyname_in_channel_listen)); - channel_free(channel); - return NULL; - } - memcpy(&server.sin_addr, - &((struct sockaddr_in *)res->ai_addr)->sin_addr, - sizeof(server.sin_addr)); - freeaddrinfo(res); -#else - if ((host = gethostbyname(hostname)) == NULL) - { - ch_error(channel, "in gethostbyname() in channel_listen()"); - PERROR(_(e_gethostbyname_in_channel_listen)); - channel_free(channel); - return NULL; - } - - char *p; - - // When using host->h_addr_list[0] directly ubsan warns for it to - // not be aligned. First copy the pointer to avoid that. - memcpy(&p, &host->h_addr_list[0], sizeof(p)); - memcpy((char *)&server.sin_addr, p, host->h_length); -#endif + server.sin_addr.s_addr = htonl(INADDR_LOOPBACK); sd = socket(AF_INET, SOCK_STREAM, 0); if (sd == -1) @@ -1632,7 +1558,7 @@ channel_listen( channel->ch_listen = TRUE; channel->CH_SOCK_FD = (sock_T)sd; channel->ch_nb_close_cb = nb_close_cb; - channel->ch_hostname = (char *)vim_strsave((char_u *)hostname); + channel->ch_hostname = (char *)vim_strsave((char_u *)""); channel->ch_port = port_in; channel->ch_to_be_closed |= (1U << PART_SOCK); @@ -3796,10 +3722,7 @@ channel_info(channel_T *channel, dict_T *dict) if (channel->ch_hostname != NULL) { if (channel->ch_port) - { - dict_add_string(dict, "hostname", (char_u *)channel->ch_hostname); dict_add_number(dict, "port", channel->ch_port); - } else // Unix-domain socket. dict_add_string(dict, "path", (char_u *)channel->ch_hostname); diff --git a/src/proto/channel.pro b/src/proto/channel.pro index d0c2cd0424..0b5d17a137 100644 --- a/src/proto/channel.pro +++ b/src/proto/channel.pro @@ -9,7 +9,7 @@ void free_unused_channels(int copyID, int mask); void channel_gui_register_all(void); channel_T *channel_open(const char *hostname, int port, int waittime, void (*nb_close_cb)(void)); channel_T *channel_listen_func(typval_T *argvars); -channel_T *channel_listen(char *hostname, int port_in, void (*nb_close_cb)(void)); +channel_T *channel_listen(int port_in, void (*nb_close_cb)(void)); channel_T *channel_listen_unix(char *path, void (*nb_close_cb)(void)); void ch_close_part(channel_T *channel, ch_part_T part); void channel_set_pipes(channel_T *channel, sock_T in, sock_T out, sock_T err); diff --git a/src/testdir/test_channel.vim b/src/testdir/test_channel.vim index 6cd55ca9f9..9110a141c7 100644 --- a/src/testdir/test_channel.vim +++ b/src/testdir/test_channel.vim @@ -2778,7 +2778,7 @@ endfunction func Test_listen() call ch_log('Test_listen()') - let server = ch_listen('127.0.0.1:12345', {'callback': function('s:test_listen_accept')}) + let server = ch_listen('12345', {'callback': function('s:test_listen_accept')}) if ch_status(server) == 'fail' call assert_report("Can't listen channel") return @@ -2799,35 +2799,29 @@ func Test_listen() call assert_match('127.0.0.1:', g:server_received_addr) endfunc -func Test_listen_invalid_address() - call ch_log('Test_listen_invalid_address()') - - " empty address - call assert_fails("call ch_listen('')", 'E475:') +func Test_listen_invalid_port() + call ch_log('Test_listen_invalid_port()') " missing port - call assert_fails("call ch_listen('localhost')", 'E475:') + call assert_fails("call ch_listen('')", 'E475:') " port number too large - call assert_fails("call ch_listen('localhost:99999')", 'E475:') + call assert_fails("call ch_listen('99999')", 'E475:') " port number zero should let the OS assign an available port - let ch = ch_listen('localhost:0') + let ch = ch_listen('0') call assert_equal('open', ch_status(ch)) call assert_notequal(0, ch_info(ch).port) call ch_close(ch) " port number negative - call assert_fails("call ch_listen('localhost:-1')", 'E475:') - - " invalid ipv6 format (missing closing bracket) - call assert_fails("call ch_listen('[::1:8765')", 'E475:') - - " invalid ipv6 format (missing port) - call assert_fails("call ch_listen('[::1]')", 'E475:') + call assert_fails("call ch_listen('-1')", 'E475:') +endfunc - " TODO: IPv6 should actually work - call assert_fails("call ch_listen('[::1]:9999')", 'E1574:') +func Test_listen_info_no_hostname() + let ch = ch_listen('0') + call assert_fails("call ch_info(ch).hostname", 'E716:') + call ch_close(ch) endfunc func Test_channel_lsp_mode() diff --git a/src/version.c b/src/version.c index 0028692f28..1694045111 100644 --- a/src/version.c +++ b/src/version.c @@ -734,6 +734,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 344, /**/ 343, /**/