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 <zdohnal@redhat.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Zdenek Dohnal
2026-04-14 16:37:25 +00:00
committed by Christian Brabandt
parent 4b6f3f1d16
commit 962a540d76
6 changed files with 41 additions and 121 deletions
+3 -2
View File
@@ -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}])
+9 -9
View File
@@ -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<any>
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|.
+14 -91
View File
@@ -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))
&& check_for_string_arg(argvars, 0) == FAIL)
return NULL;
address = tv_get_string(&argvars[0]);
if (argvars[1].v_type != VAR_UNKNOWN
&& check_for_nonnull_dict_arg(argvars, 1) == 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);
+1 -1
View File
@@ -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);
+12 -18
View File
@@ -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:')
call assert_fails("call ch_listen('-1')", 'E475:')
endfunc
" 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:')
" 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()
+2
View File
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
344,
/**/
343,
/**/