From ed78508c946d6e6fa1f792d821825a70537b815f Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Thu, 23 Jul 2026 22:48:34 +0200 Subject: [PATCH 1/6] Fail materialized FUSE writes on zero progress fuse_write_all looped "while (len > 0)" and, on a write() that returned 0, advanced by nothing and spun forever instead of reporting an error. Materializing a FUSE file for exec could therefore hang the vCPU thread. Use write_all, which treats an unexpected zero-byte write as EIO. This also removes a private copy of the same write loop. --- src/syscall/fuse.c | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/syscall/fuse.c b/src/syscall/fuse.c index d3cf4a04..1e2d230b 100644 --- a/src/syscall/fuse.c +++ b/src/syscall/fuse.c @@ -1660,22 +1660,6 @@ int fuse_access_path(const char *path, int mode, int flags) return 0; } -static int fuse_write_all(int fd, const void *buf, size_t len) -{ - const uint8_t *p = buf; - while (len > 0) { - ssize_t nw = write(fd, p, len); - if (nw < 0) { - if (errno == EINTR) - continue; - return -1; - } - p += (size_t) nw; - len -= (size_t) nw; - } - return 0; -} - static int fuse_materialize_open_file_locked(fuse_session_t *session, uint64_t nodeid, uint64_t fh, @@ -1713,7 +1697,7 @@ static int fuse_materialize_open_file_locked(fuse_session_t *session, free(reply); break; } - if (fuse_write_all(tmp_fd, reply, reply_len) < 0) { + if (write_all(tmp_fd, reply, reply_len) < 0) { rc = linux_errno(); free(reply); break; From 55c482631ea4955156327a2df60a94eeee11376a Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Thu, 23 Jul 2026 22:48:34 +0200 Subject: [PATCH 2/6] Drop sidecar_write_all for the shared write_all sidecar_write_all was a second copy of utils.h's write_all with the same short-write and EINTR handling. Call write_all instead. --- src/syscall/sidecar.c | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/syscall/sidecar.c b/src/syscall/sidecar.c index 97b3553a..b94c9c0c 100644 --- a/src/syscall/sidecar.c +++ b/src/syscall/sidecar.c @@ -1113,25 +1113,6 @@ static int sidecar_load_locked_index(int parent_dirfd, * Returns 0 on success, -1 with errno set on error. Handles short writes by * retrying until everything is committed. */ -static int sidecar_write_all(int fd, const char *buf, size_t len) -{ - size_t off = 0; - while (off < len) { - ssize_t n = write(fd, buf + off, len - off); - if (n < 0) { - if (errno == EINTR) - continue; - return -1; - } - if (n == 0) { - errno = EIO; - return -1; - } - off += (size_t) n; - } - return 0; -} - /* Serialize the index into a malloc'd buffer. *@out_len receives the byte * count. * @@ -1222,8 +1203,7 @@ static int sidecar_write_locked_index(int parent_dirfd, errno = saved_errno; return -1; } - if (payload_len > 0 && - sidecar_write_all(tmp_fd, payload, payload_len) < 0) { + if (payload_len > 0 && write_all(tmp_fd, payload, payload_len) < 0) { int saved_errno = errno; close(tmp_fd); (void) unlinkat(parent_dirfd, SIDECAR_INDEX_TMP_NAME, 0); From 76b0383d88171704514f97fc5279e2a35d2e81f4 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Thu, 23 Jul 2026 22:48:34 +0200 Subject: [PATCH 3/6] Use the ALIGN_2MIB macros in the Rosetta loader align2m_up and align2m_down duplicated the ALIGN_2MIB_UP and ALIGN_2MIB_DOWN macros in utils.h, and two more sites open-coded the "& ~(BLOCK_2MIB - 1)" mask. Use the macros so 2 MiB rounding has one definition. --- src/core/rosetta.c | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/core/rosetta.c b/src/core/rosetta.c index b8be95e0..2a93eaaa 100644 --- a/src/core/rosetta.c +++ b/src/core/rosetta.c @@ -49,17 +49,6 @@ #include "syscall/internal.h" /* fd_alloc_from, fd_publish_linux_flags, FD_REGULAR */ #include "syscall/proc.h" /* proc_set_cmdline */ -/* Round a guest virtual address (or size) up to the next 2 MiB boundary. */ -static inline uint64_t align2m_up(uint64_t v) -{ - return (v + BLOCK_2MIB - 1) & ~(BLOCK_2MIB - 1); -} - -static inline uint64_t align2m_down(uint64_t v) -{ - return v & ~(BLOCK_2MIB - 1); -} - /* The VZ_CAPS payload only has room for a 42-byte inline path. Publish * /proc/self/fd/ there when the real host path is longer so * rosetta sees a valid reopenable path without truncation, while the host-side @@ -227,8 +216,8 @@ int rosetta_prepare(guest_t *g, * range above that. The mapping must cover the entire span so all segments * resolve through a single Stage-2 region. */ - uint64_t va_base = align2m_down(ri->load_min); - uint64_t va_end = align2m_up(ri->load_max); + uint64_t va_base = ALIGN_2MIB_DOWN(ri->load_min); + uint64_t va_end = ALIGN_2MIB_UP(ri->load_max); if (va_end <= va_base) { log_error("rosetta: empty load range"); return -1; @@ -258,7 +247,7 @@ int rosetta_prepare(guest_t *g, (unsigned long long) g->interp_base); return -1; } - guest_base = (rosetta_limit - size) & ~(BLOCK_2MIB - 1); + guest_base = ALIGN_2MIB_DOWN(rosetta_limit - size); if (guest_base < g->stack_top + BLOCK_2MIB) { log_error( "rosetta: no image gap between stack_top=0x%llx and " @@ -296,7 +285,7 @@ int rosetta_prepare(guest_t *g, (unsigned long long) guest_base); return -1; } - uint64_t kbuf_gpa = (guest_base - KBUF_SIZE) & ~(BLOCK_2MIB - 1); + uint64_t kbuf_gpa = ALIGN_2MIB_DOWN(guest_base - KBUF_SIZE); if (guest_init_kbuf(g, kbuf_gpa) < 0) { log_error("rosetta: guest_init_kbuf failed at 0x%llx", (unsigned long long) kbuf_gpa); From d9c19b757aacd6228963fcc9ec54a0fb96e31713 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Thu, 23 Jul 2026 22:48:34 +0200 Subject: [PATCH 4/6] Use the fd flag helpers instead of open-coding fcntl The FIONBIO handler open-coded an F_GETFL / modify / F_SETFL sequence, and two pty-keepalive paths open-coded F_GETFD / F_SETFD | FD_CLOEXEC. utils.h already provides fd_update_status_flag and fd_set_cloexec for exactly these; use them. --- src/runtime/procemu.c | 11 +++-------- src/syscall/io.c | 8 +------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/runtime/procemu.c b/src/runtime/procemu.c index a8fbe273..ed83fcc7 100644 --- a/src/runtime/procemu.c +++ b/src/runtime/procemu.c @@ -2120,8 +2120,7 @@ void proc_pty_dup_keepalive_locked(int src_master_host_fd, /* dup(2) clears FD_CLOEXEC; the keepalive must not survive exec into a * guest child that has no map back to it. */ - int fdflags = fcntl(dst_slave, F_GETFD); - if (fdflags < 0 || fcntl(dst_slave, F_SETFD, fdflags | FD_CLOEXEC) < 0) { + if (fd_set_cloexec(dst_slave) < 0) { close(dst_slave); return; } @@ -2246,12 +2245,8 @@ void proc_pty_restore_keepalive(int master_host_fd, if (master_host_fd < 0) goto drop; - if (slave_host_fd >= 0) { - int fdflags = fcntl(slave_host_fd, F_GETFD); - if (fdflags < 0 || - fcntl(slave_host_fd, F_SETFD, fdflags | FD_CLOEXEC) < 0) - goto drop; - } + if (slave_host_fd >= 0 && fd_set_cloexec(slave_host_fd) < 0) + goto drop; /* Trust the parent's linux_pts_num verbatim instead of re-parsing * slave_path. The wire-format string is bounded to PTY_SLAVE_PATH_MAX - 1 diff --git a/src/syscall/io.c b/src/syscall/io.c index a000f0e9..ef2e34ef 100644 --- a/src/syscall/io.c +++ b/src/syscall/io.c @@ -2303,13 +2303,7 @@ int64_t sys_ioctl(guest_t *g, int fd, uint64_t request, uint64_t arg) host_fd_ref_close(&host_ref); return -LINUX_EFAULT; } - int flags = fcntl(host_fd, F_GETFL); - if (flags < 0) { - host_fd_ref_close(&host_ref); - return linux_errno(); - } - flags = on ? (flags | O_NONBLOCK) : (flags & ~O_NONBLOCK); - int r = fcntl(host_fd, F_SETFL, flags); + int r = fd_update_status_flag(host_fd, O_NONBLOCK, on != 0); host_fd_ref_close(&host_ref); return r < 0 ? linux_errno() : 0; } From 06d1364dabb3e95770d6fcb90e69a132cd725300 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Thu, 23 Jul 2026 22:48:48 +0200 Subject: [PATCH 5/6] Add path_component_copy for counted path components Five component walks in path.c and sidecar.c open-coded the same block: declare a NAME_MAX+1 buffer, reject an overlong component with ENAMETOOLONG, then memcpy the counted component and NUL-terminate. The bound check was even spelled two ways ("len > NAME_MAX" and "comp_len >= sizeof(buf)"). Fold that into a path_component_copy helper next to path_next_component, which produces the counted, non-NUL-terminated components these sites consume. Callers keep their own cleanup on the error path and gate it on the helper's return. --- src/syscall/path.c | 17 +++-------------- src/syscall/path.h | 22 ++++++++++++++++++++++ src/syscall/sidecar.c | 13 ++++--------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/syscall/path.c b/src/syscall/path.c index 76541b8d..2abbe415 100644 --- a/src/syscall/path.c +++ b/src/syscall/path.c @@ -418,13 +418,10 @@ int sys_path_has_symlink(guest_fd_t dirfd, const char *path) continue; char name[NAME_MAX + 1]; - if (len > NAME_MAX) { - errno = ENAMETOOLONG; + if (path_component_copy(name, sizeof(name), comp, len) < 0) { rc = -1; goto out; } - memcpy(name, comp, len); - name[len] = '\0'; struct stat st; if (fstatat(current_fd, name, &st, AT_SYMLINK_NOFOLLOW) < 0) { @@ -1217,12 +1214,8 @@ int path_openat2_crosses_mount(guest_fd_t dirfd, } else { char name[NAME_MAX + 1]; char parent[LINUX_PATH_MAX]; - if (len > NAME_MAX) { - errno = ENAMETOOLONG; + if (path_component_copy(name, sizeof(name), comp, len) < 0) goto out; - } - memcpy(name, comp, len); - name[len] = '\0'; if (str_copy_trunc(parent, current, sizeof(parent)) >= sizeof(parent)) { errno = ENAMETOOLONG; @@ -1316,12 +1309,8 @@ int path_openat2_crosses_mount(guest_fd_t dirfd, if (host_walk && *rest != '\0' && !(len == 2 && comp[0] == '.' && comp[1] == '.')) { char name[NAME_MAX + 1]; - if (len > NAME_MAX) { - errno = ENAMETOOLONG; + if (path_component_copy(name, sizeof(name), comp, len) < 0) goto out; - } - memcpy(name, comp, len); - name[len] = '\0'; host_fd_t next_fd = openat(current_fd, name, O_RDONLY | O_DIRECTORY | O_CLOEXEC); if (replace_walk_fd(¤t_fd, next_fd) < 0) diff --git a/src/syscall/path.h b/src/syscall/path.h index 61d3b8cc..dd5db372 100644 --- a/src/syscall/path.h +++ b/src/syscall/path.h @@ -7,10 +7,12 @@ #pragma once +#include #include #include #include #include +#include #include #include "syscall/internal.h" @@ -62,6 +64,26 @@ static inline int path_translation_at_flags(const path_translation_t *tx, */ bool path_next_component(const char **pathp, const char **comp, size_t *len); +/* Copy a counted component (not NUL-terminated, as path_next_component reports) + * into dst and NUL-terminate it. Returns 0, or -1 with errno set to + * ENAMETOOLONG when the component does not fit. dst is typically a NAME_MAX+1 + * buffer, so this also unifies the "len > NAME_MAX" and "len >= sizeof(dst)" + * bound checks the callers used to spell inconsistently. + */ +static inline int path_component_copy(char *dst, + size_t dstsz, + const char *comp, + size_t len) +{ + if (len >= dstsz) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dst, comp, len); + dst[len] = '\0'; + return 0; +} + bool path_might_use_open_intercept(const char *path); bool path_might_use_stat_intercept(const char *path); int path_check_intercept_access(const struct stat *st, int mode, int flags); diff --git a/src/syscall/sidecar.c b/src/syscall/sidecar.c index b94c9c0c..7bc72a51 100644 --- a/src/syscall/sidecar.c +++ b/src/syscall/sidecar.c @@ -676,13 +676,11 @@ int sidecar_translate_lookup_at(guest_fd_t dirfd, size_t comp_len; while (path_next_component(&scan, &comp, &comp_len)) { char guest_comp[NAME_MAX + 1]; - if (comp_len >= sizeof(guest_comp)) { + if (path_component_copy(guest_comp, sizeof(guest_comp), comp, + comp_len) < 0) { close(cur_fd); - errno = ENAMETOOLONG; return -1; } - memcpy(guest_comp, comp, comp_len); - guest_comp[comp_len] = '\0'; if (sidecar_name_reserved(guest_comp)) { close(cur_fd); @@ -809,12 +807,9 @@ int sidecar_translate_lookup_at(guest_fd_t dirfd, */ while (path_next_component(&scan, &comp, &comp_len)) { char rest_comp[NAME_MAX + 1]; - if (comp_len >= sizeof(rest_comp)) { - errno = ENAMETOOLONG; + if (path_component_copy(rest_comp, sizeof(rest_comp), comp, + comp_len) < 0) return -1; - } - memcpy(rest_comp, comp, comp_len); - rest_comp[comp_len] = '\0'; if (sidecar_append_component(out, outsz, &out_len, rest_comp, absolute) < 0) return -1; From 76d9378be7f7a12a4a6c1681cce802e1bcfee8e5 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Thu, 23 Jul 2026 22:48:49 +0200 Subject: [PATCH 6/6] Share hex_nibble as the companion to bytes_to_hex gdbstub-rsp.c (hex_val) and sidecar.c (hex_nibble) each carried the same hex-digit decode ladder, the inverse of the bytes_to_hex encoder already in utils.h. Move it there as hex_nibble and drop both copies. gdb_hex_encode already uses bytes_to_hex, so the two now share both directions of the codec. --- src/debug/gdbstub-rsp.c | 19 +++++-------------- src/syscall/sidecar.c | 11 ----------- src/utils.h | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/debug/gdbstub-rsp.c b/src/debug/gdbstub-rsp.c index c2512bc6..b0db730d 100644 --- a/src/debug/gdbstub-rsp.c +++ b/src/debug/gdbstub-rsp.c @@ -18,17 +18,6 @@ static const char hex_chars[] = "0123456789abcdef"; -static int hex_val(char c) -{ - if (c >= '0' && c <= '9') - return c - '0'; - if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - return -1; -} - int gdb_hex_encode(char *dst, const uint8_t *src, size_t len) { return (int) bytes_to_hex(dst, src, len); @@ -37,7 +26,8 @@ int gdb_hex_encode(char *dst, const uint8_t *src, size_t len) int gdb_hex_decode(uint8_t *dst, const char *src, size_t len) { for (size_t i = 0; i < len; i++) { - int hi = hex_val(src[i * 2]), lo = hex_val(src[i * 2 + 1]); + int hi = hex_nibble((unsigned char) src[i * 2]), + lo = hex_nibble((unsigned char) src[i * 2 + 1]); if (hi < 0 || lo < 0) return -1; dst[i] = (uint8_t) ((hi << 4) | lo); @@ -50,7 +40,7 @@ uint64_t gdb_parse_hex(const char **pp) const char *p = *pp; uint64_t val = 0; while (1) { - int d = hex_val(*p); + int d = hex_nibble((unsigned char) *p); if (d < 0) break; val = (val << 4) | (uint64_t) d; @@ -209,7 +199,8 @@ int gdb_rsp_recv(gdb_rsp_ctx_t *ctx, int fd, char *buf, size_t bufsz) buf[pos] = '\0'; uint8_t expected = - (uint8_t) ((hex_val(ck_hi) << 4) | hex_val(ck_lo)); + (uint8_t) ((hex_nibble((unsigned char) ck_hi) << 4) | + hex_nibble((unsigned char) ck_lo)); uint8_t actual = rsp_checksum(buf, pos); if (expected == actual) { if (!ctx->no_ack_mode) diff --git a/src/syscall/sidecar.c b/src/syscall/sidecar.c index 7bc72a51..32cffff0 100644 --- a/src/syscall/sidecar.c +++ b/src/syscall/sidecar.c @@ -295,17 +295,6 @@ bool sidecar_path_targets_reserved_name(const char *path) return sidecar_name_reserved(basename); } -static int hex_nibble(unsigned char c) -{ - if (c >= '0' && c <= '9') - return (int) (c - '0'); - if (c >= 'a' && c <= 'f') - return (int) (c - 'a' + 10); - if (c >= 'A' && c <= 'F') - return (int) (c - 'A' + 10); - return -1; -} - static int sidecar_decode_name(const char *hex, char **out) { size_t len = strlen(hex); diff --git a/src/utils.h b/src/utils.h index c4d87b71..694bfeb5 100644 --- a/src/utils.h +++ b/src/utils.h @@ -127,6 +127,20 @@ static inline size_t bytes_to_hex(char *dst, const uint8_t *src, size_t len) return len * 2; } +/* Decode a single hex digit to its 0-15 value, or -1 if @c is not a hex digit. + * The inverse building block of bytes_to_hex; accepts either case. + */ +static inline int hex_nibble(unsigned char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + return -1; +} + /* Write exactly @len bytes to a blocking @fd, resuming across short writes and * EINTR. Returns 0 once every byte is written, or -1 with errno set on error. * An unexpected zero-byte return is treated as EIO rather than spun on, since