• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

systemd / systemd / 23415972010

22 Mar 2026 07:07PM UTC coverage: 72.581% (-0.03%) from 72.606%
23415972010

push

github

web-flow
ssh: handle VMADDR_CID_ANY in a couple places (#41230)

Originally reported in Ubuntu as
https://bugs.launchpad.net/ubuntu/+source/systemd/+bug/2145027.

1 of 3 new or added lines in 2 files covered. (33.33%)

965 existing lines in 60 files now uncovered.

316346 of 435854 relevant lines covered (72.58%)

1154016.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

12.61
/src/ssh-generator/ssh-proxy.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <unistd.h>
4

5
#include "sd-varlink.h"
6

7
#include "alloc-util.h"
8
#include "fd-util.h"
9
#include "io-util.h"
10
#include "iovec-util.h"
11
#include "log.h"
12
#include "main-func.h"
13
#include "path-lookup.h"
14
#include "path-util.h"
15
#include "socket-util.h"
16
#include "stat-util.h"
17
#include "string-util.h"
18
#include "strv.h"
19
#include "time-util.h"
20

21
#define HEADER_READ_TIMEOUT_USEC (5 * USEC_PER_SEC)
22

23
static int process_vsock_cid(unsigned cid, const char *port) {
×
24
        int r;
×
25

26
        assert(cid != VMADDR_CID_ANY);
×
27
        assert(port);
×
28

29
        union sockaddr_union sa = {
×
30
                .vm.svm_cid = cid,
31
                .vm.svm_family = AF_VSOCK,
32
        };
33

34
        r = vsock_parse_port(port, &sa.vm.svm_port);
×
35
        if (r < 0)
×
36
                return log_error_errno(r, "Failed to parse vsock port: %s", port);
×
37

38
        _cleanup_close_ int fd = socket(AF_VSOCK, SOCK_STREAM|SOCK_CLOEXEC, 0);
×
39
        if (fd < 0)
×
40
                return log_error_errno(errno, "Failed to allocate AF_VSOCK socket: %m");
×
41

42
        if (connect(fd, &sa.sa, sockaddr_len(&sa)) < 0)
×
43
                return log_error_errno(errno, "Failed to connect to vsock:%u:%u: %m", sa.vm.svm_cid, sa.vm.svm_port);
×
44

45
        /* OpenSSH wants us to send a single byte along with the file descriptor, hence do so */
46
        r = send_one_fd_iov(STDOUT_FILENO, fd, &iovec_nul_byte, /* iovlen= */ 1, /* flags= */ 0);
×
47
        if (r < 0)
×
48
                return log_error_errno(r, "Failed to send socket via STDOUT: %m");
×
49

50
        log_debug("Successfully sent AF_VSOCK socket via STDOUT.");
×
51
        return 0;
52
}
53

54
static int process_vsock_string(const char *host, const char *port) {
×
55
        unsigned cid;
×
56
        int r;
×
57

58
        assert(host);
×
59
        assert(port);
×
60

61
        r = vsock_parse_cid(host, &cid);
×
62
        if (r < 0)
×
63
                return log_error_errno(r, "Failed to parse vsock cid: %s", host);
×
64

NEW
65
        if (cid == VMADDR_CID_ANY)
×
NEW
66
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use VMADDR_CID_ANY to connect to a remote host.");
×
67

UNCOV
68
        return process_vsock_cid(cid, port);
×
69
}
70

71
static int process_unix(const char *path) {
3✔
72
        int r;
3✔
73

74
        assert(path);
3✔
75

76
        /* We assume the path is absolute unless it starts with a dot (or is already explicitly absolute) */
77
        _cleanup_free_ char *prefixed = NULL;
3✔
78
        if (!STARTSWITH_SET(path, "/", "./")) {
3✔
79
                prefixed = strjoin("/", path);
3✔
80
                if (!prefixed)
3✔
81
                        return log_oom();
×
82

83
                path = prefixed;
84
        }
85

86
        _cleanup_close_ int fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
6✔
87
        if (fd < 0)
3✔
88
                return log_error_errno(errno, "Failed to allocate AF_UNIX socket: %m");
×
89

90
        r = connect_unix_path(fd, AT_FDCWD, path);
3✔
91
        if (r < 0)
3✔
92
                return log_error_errno(r, "Failed to connect to AF_UNIX socket %s: %m", path);
×
93

94
        r = send_one_fd_iov(STDOUT_FILENO, fd, &iovec_nul_byte, /* iovlen= */ 1, /* flags= */ 0);
3✔
95
        if (r < 0)
3✔
96
                return log_error_errno(r, "Failed to send socket via STDOUT: %m");
×
97

98
        log_debug("Successfully sent AF_UNIX socket via STDOUT.");
3✔
99
        return 0;
100
}
101

102
static int skip_ok_port_res(int fd, const char *path, const char *port) {
×
103
        struct timeval oldtv;
×
104
        socklen_t oldtv_size = sizeof(oldtv);
×
105
        if (getsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &oldtv, &oldtv_size) < 0)
×
106
                return log_error_errno(errno, "Failed to get socket receive timeout for %s: %m", path);
×
107
        if (oldtv_size != sizeof(oldtv))
×
108
                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Unexpected size of socket receive timeout for %s", path);
×
109
        if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, TIMEVAL_STORE(HEADER_READ_TIMEOUT_USEC), sizeof(struct timeval)) < 0)
×
110
                return log_error_errno(errno, "Failed to set socket receive timeout for %s: %m", path);
×
111

112
        char recv_buf[STRLEN("OK 65535\n")];
×
113
        size_t bytes_recv = 0, bytes_avail = 0, pos = 0;
×
114
        static const char expected_prefix[] = "OK ";
×
115

116
        for (;;) {
×
117
                if (pos >= bytes_avail) {
×
118
                        assert(bytes_recv <= bytes_avail);
×
119
                        if (bytes_avail >= sizeof(recv_buf)) {
×
120
                                /*
121
                                  Full buffer means that we have peeked as many bytes as possible and not seeing the ending \n .
122
                                  So the server is believed to not send OK PORT response, and we just pass out the socket to ssh client,
123
                                  and let it handle the connection.
124

125
                                  If we have not received any bytes from the socket buffer, we can safely pass out the socket,
126
                                  since no change has been made to the socket buffer. Otherwise, if some bytes have been received,
127
                                  the socket buffer has been changed, the only option is to give up and terminate the connection.
128
                                  Similar logic applies below when we meet other kinds of unexpected responses.
129
                                */
130
                                if (bytes_recv == 0) {
×
131
                                        log_debug("Received too many bytes while waiting for OK PORT response from %s\n"
×
132
                                                  "Assume the multiplexer is not sending OK PORT.",
133
                                                  path);
134
                                        goto passout_fd;
×
135
                                }
136
                                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Received too many bytes while waiting for OK PORT response from %s", path);
×
137
                        }
138
                        if (bytes_avail > bytes_recv) {
×
139
                                /*
140
                                  Discard already peeked bytes before peeking more.
141

142
                                  XXX: We cannot use SO_RCVLOWAT to set the minimum number of bytes to be peeked to peek entire
143
                                       OK PORT response at once to prevent changes to the recving buffer, because SO_RCVLOWAT
144
                                       does not work on unix sockets with recv(..., MSG_PEEK). Also poll() does not help here,
145
                                       because poll() returns readable as long as there is any data in the socket buffer for
146
                                       unix sockets, not respecting SO_RCVLOWAT.
147

148
                                  XXX: We could have used SO_PEEK_OFF to continuously peek more data without changing the socket
149
                                       receive buffer, but this function breaks since Linux 4.3 due to a kernel bug, which is fixed
150
                                       in Linux 6.18 commit 7bf3a476ce43 ("af_unix: Read sk_peek_offset() again after sleeping in
151
                                       unix_stream_read_generic()."). It is also not possible to detect whether the kernel is
152
                                       affected by this bug at runtime.
153

154
                                  As a result, we have no other choice but to discard already peeked data here.
155
                                */
156
                                ssize_t rlen = recv(fd, recv_buf + bytes_recv, bytes_avail - bytes_recv, /* flags= */ 0);
×
157
                                if (rlen < 0)
×
158
                                        return log_error_errno(errno, "Failed to discard OK PORT response from %s: %m", path);
×
159
                                if ((size_t) rlen != bytes_avail - bytes_recv)
×
160
                                        return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while discarding OK PORT response from %s", path);
×
161
                                log_debug("Successfully discarded %zi bytes of response: %.*s", rlen, (int) rlen, recv_buf + bytes_recv);
×
162
                                bytes_recv = bytes_avail;
163
                        }
164
                        ssize_t len = recv(fd, recv_buf + bytes_avail, sizeof(recv_buf) - bytes_avail, MSG_PEEK);
×
165
                        if (len < 0) {
×
166
                                if (errno != EAGAIN)
×
167
                                        return log_error_errno(errno, "Failed to receive OK from %s: %m", path);
×
168
                                if (bytes_recv == 0) {
×
169
                                        log_debug("Timeout while waiting for OK PORT response from %s\n"
×
170
                                                  "Assume the multiplexer will not send OK PORT.",
171
                                                  path);
172
                                        goto passout_fd;
×
173
                                }
174
                                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Timed out to receive OK PORT from %s", path);
×
175

176
                        }
177
                        if (len == 0) {
×
178
                                log_debug("Connection closed while waiting for OK PORT response from %s", path);
×
179
                                if (bytes_recv == 0) {
×
180
                                        log_debug("No data received, which means the connecting port is not open.");
×
181
                                        return log_error_errno(SYNTHETIC_ERRNO(ECONNREFUSED), "Port %s on %s is not open", port, path);
×
182
                                }
183
                                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Connection closed before full OK PORT response received from %s.", path);
×
184
                        }
185
                        bytes_avail += len;
×
186
                }
187
                assert(pos < bytes_avail);
×
188
                if (pos < strlen(expected_prefix) && recv_buf[pos] != expected_prefix[pos]) {
×
189
                        if (bytes_recv == 0) {
×
190
                                log_debug("Received response does not start with expected OK PORT response from %s\n"
×
191
                                          "Assume the multiplexer will not send OK PORT.",
192
                                          path);
193
                                goto passout_fd;
×
194
                        }
195
                        return log_error_errno(SYNTHETIC_ERRNO(EIO), "Received invalid response while waiting for OK PORT from %s", path);
×
196
                }
197
                if (recv_buf[pos] == '\n') {
×
198
                        pos += 1;
×
199
                        break;
×
200
                }
201
                pos += 1;
×
202
        }
203

204
        assert(pos <= sizeof(recv_buf));
×
205
        assert(bytes_recv <= pos);
×
206
        if (bytes_recv < pos) {
×
207
                ssize_t len = recv(fd, recv_buf + bytes_recv, pos - bytes_recv, /* flags= */ 0);
×
208
                if (len < 0)
×
209
                        return log_error_errno(errno, "Failed to discard OK PORT response from %s: %m", path);
×
210
                if ((size_t) len != pos - bytes_recv)
×
211
                        return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while discarding OK PORT response from %s", path);
×
212
                log_debug("Successfully discarded response from %s: %.*s", path, (int) pos, recv_buf);
×
213
        }
214

215
passout_fd:
×
216
        if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &oldtv, sizeof(oldtv)) < 0)
×
217
                return log_error_errno(errno, "Failed to restore socket receive timeout for %s: %m", path);
×
218
        return 0;
219
}
220

221
static int process_vsock_mux(const char *path, const char *port) {
×
222
        int r;
×
223

224
        assert(path);
×
225
        assert(port);
×
226

227
        /* We assume the path is absolute unless it starts with a dot (or is already explicitly absolute) */
228
        _cleanup_free_ char *prefixed = NULL;
×
229
        if (!STARTSWITH_SET(path, "/", "./")) {
×
230
                prefixed = strjoin("/", path);
×
231
                if (!prefixed)
×
232
                        return log_oom();
×
233

234
                path = prefixed;
235
        }
236

237
        _cleanup_close_ int fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
×
238
        if (fd < 0)
×
239
                return log_error_errno(errno, "Failed to allocate AF_UNIX socket: %m");
×
240

241
        r = connect_unix_path(fd, AT_FDCWD, path);
×
242
        if (r < 0)
×
243
                return log_error_errno(r, "Failed to connect to AF_UNIX socket %s: %m", path);
×
244

245
        /* Based on the protocol as defined here:
246
         * https://github.com/cloud-hypervisor/cloud-hypervisor/blob/main/docs/vsock.md
247
         * https://github.com/firecracker-microvm/firecracker/blob/main/docs/vsock.md */
248
        _cleanup_free_ char *connect_cmd = NULL;
×
249
        connect_cmd = strjoin("CONNECT ", port, "\n");
×
250
        if (!connect_cmd)
×
251
                return log_oom();
×
252

253
        r = loop_write(fd, connect_cmd, SIZE_MAX);
×
254
        if (r < 0)
×
255
                return log_error_errno(r, "Failed to send CONNECT to %s:%s: %m", path, port);
×
256

257
        r = skip_ok_port_res(fd, path, port);
×
258
        if (r < 0)
×
259
                return r;
260

261
        r = send_one_fd_iov(STDOUT_FILENO, fd, &iovec_nul_byte, /* iovlen= */ 1, /* flags= */ 0);
×
262
        if (r < 0)
×
263
                return log_error_errno(r, "Failed to send socket via STDOUT: %m");
×
264

265
        log_debug("Successfully sent AF_UNIX socket via STDOUT.");
×
266
        return 0;
267
}
268

269
static int fetch_machine(const char *machine, RuntimeScope scope, sd_json_variant **ret) {
×
270
        int r;
×
271

272
        assert(machine);
×
273
        assert(ret);
×
274

275
        _cleanup_free_ char *addr = NULL;
×
276
        r = runtime_directory_generic(scope, "systemd/machine/io.systemd.Machine", &addr);
×
277
        if (r < 0)
×
278
                return r;
279

280
        _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
×
281
        r = sd_varlink_connect_address(&vl, addr);
×
282
        if (r < 0)
×
283
                return log_error_errno(r, "Failed to connect to machined on %s: %m", addr);
×
284

285
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL;
×
286
        const char *error_id;
×
287
        r = sd_varlink_callbo(
×
288
                        vl,
289
                        "io.systemd.Machine.List",
290
                        &result,
291
                        &error_id,
292
                        SD_JSON_BUILD_PAIR_STRING("name", machine));
293
        if (r < 0)
×
294
                return log_error_errno(r, "Failed to issue io.systemd.Machine.List() varlink call: %m");
×
295
        if (error_id) {
×
296
                if (streq(error_id, "io.systemd.Machine.NoSuchMachine"))
×
297
                        return -ESRCH;
298

299
                r = sd_varlink_error_to_errno(error_id, result); /* If this is a system errno style error, output it with %m */
×
300
                if (r != -EBADR)
×
301
                        return log_error_errno(r, "Failed to issue io.systemd.Machine.List() varlink call: %m");
×
302

303
                return log_error_errno(r, "Failed to issue io.systemd.Machine.List() varlink call: %s", error_id);
×
304
        }
305

306
        *ret = TAKE_PTR(result);
×
307
        return 0;
×
308
}
309

310
static int process_machine(const char *machine, const char *port) {
×
311
        int r;
×
312

313
        assert(machine);
×
314
        assert(port);
×
315

316
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL;
×
317
        RuntimeScope scope = RUNTIME_SCOPE_USER;
×
318
        r = fetch_machine(machine, scope, &result);
×
319
        if (r == -ESRCH) {
×
320
                scope = RUNTIME_SCOPE_SYSTEM;
×
321
                r = fetch_machine(machine, scope, &result);
×
322
        }
323
        if (r < 0)
×
324
                return r;
325

326
        struct {
×
327
                uint32_t cid;
328
                const char *class;
329
                const char *service;
330
        } p = {
×
331
                .cid = VMADDR_CID_ANY,
332
        };
333

334
        static const sd_json_dispatch_field dispatch_table[] = {
×
335
                { "vSockCid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint32,       voffsetof(p, cid),     0                 },
336
                { "class",    SD_JSON_VARIANT_STRING,   sd_json_dispatch_const_string, voffsetof(p, class),   SD_JSON_MANDATORY },
337
                { "service",  SD_JSON_VARIANT_STRING,   sd_json_dispatch_const_string, voffsetof(p, service), 0                 },
338
        };
339

340
        r = sd_json_dispatch(result, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
×
341
        if (r < 0)
×
342
                return log_error_errno(r, "Failed to parse Varlink reply: %m");
×
343

344
        if (streq(p.class, "container")) {
×
345
                _cleanup_free_ char *path = NULL;
×
346

347
                if (!streq_ptr(p.service, "systemd-nspawn"))
×
348
                        return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Don't know how to SSH into '%s' container %s.", p.service, machine);
×
349

350
                r = runtime_directory_generic(scope, "systemd/nspawn/unix-export", &path);
×
351
                if (r < 0)
×
352
                        return log_error_errno(r, "Failed to determine runtime directory: %m");
×
353

354
                if (!path_extend(&path, machine, "ssh"))
×
355
                        return log_oom();
×
356

357
                r = is_socket(path);
×
358
                if (r < 0)
×
359
                        return log_error_errno(r, "Failed to check if '%s' exists and is a socket: %m", path);
×
360
                if (r == 0)
×
361
                        return log_error_errno(
×
362
                                        SYNTHETIC_ERRNO(ENOENT),
363
                                        "'%s' does not exist or is not a socket, are sshd and systemd-ssh-generator installed and enabled in the container?",
364
                                        path);
365

366
                return process_unix(path);
×
367
        }
368

369
        if (!streq(p.class, "vm"))
×
370
                return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Don't know how to SSH into machine %s with class '%s'.", machine, p.class);
×
371

372
        if (p.cid == VMADDR_CID_ANY)
×
373
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Machine %s has no AF_VSOCK CID assigned.", machine);
×
374

375
        return process_vsock_cid(p.cid, port);
×
376
}
377

378
static char *startswith_sep(const char *s, const char *prefix) {
6✔
379
        const char *p = startswith(s, prefix);
6✔
380

381
        if (p && IN_SET(*p, '/', '%'))
6✔
382
                return (char*) p + 1;
3✔
383

384
        return NULL;
385
}
386

387
static int run(int argc, char* argv[]) {
3✔
388

389
        log_setup();
3✔
390

391
        if (argc != 3)
3✔
392
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected two arguments: host and port.");
×
393

394
        const char *host = argv[1], *port = argv[2];
3✔
395

396
        const char *p = startswith_sep(host, "vsock");
3✔
397
        if (p)
3✔
398
                return process_vsock_string(p, port);
×
399

400
        p = startswith_sep(host, "unix");
3✔
401
        if (p)
3✔
402
                return process_unix(p);
3✔
403

404
        p = startswith_sep(host, "vsock-mux");
×
405
        if (p)
×
406
                return process_vsock_mux(p, port);
×
407

408
        p = startswith_sep(host, "machine");
×
409
        if (p)
×
410
                return process_machine(p, port);
×
411

412
        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Don't know how to parse host name specification: %s", host);
×
413
}
414

415
DEFINE_MAIN_FUNCTION(run);
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc