From 07955929ee0005a7394ae24eb209f0298654ab0c Mon Sep 17 00:00:00 2001 From: Max042004 Date: Sat, 11 Jul 2026 23:31:07 +0800 Subject: [PATCH] Route sys_statfs through the /proc intercept /proc entries are synthesized on open with no backing file under the sysroot (see proc_intercept_open), so a raw statfs() on any /proc path -- including the literal "/proc" -- fails with ENOENT even though stat() and open() on the same path succeed. Confirmed by cross-compiling a guest probe and running it under elfuse: stat("/proc") succeeded while statfs("/proc") returned ENOENT. Fix sys_statfs to route /proc paths through proc_intercept_open, fstatfs the resulting host fd, and close it, mirroring how sys_newfstatat already resolves /proc via proc_intercept_stat. The prefix check is boundary-safe (rejects "/procfoo") to avoid reintroducing the naive-match class of bug the issue warns about. While reproducing this, found that proc_intercept_open's bare "/proc" case lacked the trailing-slash variant present on all its children (e.g. "/proc/self/"), so statfs("/proc/") missed even after the fix above; stat() already handled it via proc_intercept_stat's separate check. Added the missing variant for consistency. Review flagged that routing /proc/self/fd and /proc/self/fdinfo through proc_intercept_open makes statfs() pay for those nodes' per-open scratch directory -- one placeholder file per live guest fd, needed only so getdents has something to list -- even though statfs() never enumerates it. Past PROC_SCRATCH_DIRS_MAX untracked opens, the leaked dirs also stop getting cleaned up at exit. Add proc_intercept_statfs, a lightweight resolver that recognizes just those two nodes and statfs()es /tmp directly (every scratch dir lives under /tmp, so the filesystem info is identical); sys_statfs tries it first and only falls back to proc_intercept_open for every other /proc path. Add regression coverage to test-proc.c: statfs on "/proc", "/proc/", and a synthetic file, plus an f_type cross-check against fstatfs on an already-open fd of the same path; repeated statfs on /proc/self/fd and /proc/self/fdinfo past the scratch-dir tracking cap. --- src/runtime/procemu.c | 35 +++++++++++++++++- src/runtime/procemu.h | 11 ++++++ src/syscall/fs-stat.c | 42 +++++++++++++++++++++- tests/test-proc.c | 83 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 168 insertions(+), 3 deletions(-) diff --git a/src/runtime/procemu.c b/src/runtime/procemu.c index 28f80668..19b166df 100644 --- a/src/runtime/procemu.c +++ b/src/runtime/procemu.c @@ -2801,7 +2801,7 @@ int proc_intercept_open(const guest_t *g, * "self" symlink. The DIR* created from this allows getdents64 to enumerate * /proc like a real procfs. Cleaned up via atexit. */ - if (!strcmp(path, "/proc")) { + if (!strcmp(path, "/proc") || !strcmp(path, "/proc/")) { const char *dir = ensure_proc_tmpdir(g); if (!dir) return -1; @@ -3436,6 +3436,39 @@ int proc_intercept_open(const guest_t *g, return PROC_NOT_INTERCEPTED; } +int proc_intercept_statfs(const char *path, struct statfs *out) +{ + /* /proc/[/...] -> /proc/self[...], same alias as + * proc_intercept_open/proc_intercept_stat. + */ + char alias[LINUX_PATH_MAX]; + int aliased = proc_alias_self(path, alias, sizeof(alias)); + if (aliased < 0) + return -1; + if (aliased > 0) + return proc_intercept_statfs(alias, out); + + /* /proc/self/fd and /proc/self/fdinfo are the only /proc nodes whose open + * path allocates per-call scratch state: proc_open_fd_scratch mkdtemp's a + * fresh directory and creates one placeholder file per live guest fd on + * every single open, purely so getdents has something to enumerate. A + * plain statfs() never enumerates the directory, so paying for that + * allocation just to fstatfs() the result and immediately discard it is + * wasted work -- and once PROC_SCRATCH_DIRS_MAX untracked opens + * accumulate, the atexit cleanup no longer knows about further dirs and + * they leak in /tmp permanently. + * + * Every scratch dir (like every other synthetic /proc node) is created + * under /tmp, so statfs("/tmp") reports identical filesystem info without + * allocating anything. + */ + if (strcmp(path, "/proc/self/fd") && strcmp(path, "/proc/self/fd/") && + strcmp(path, "/proc/self/fdinfo") && strcmp(path, "/proc/self/fdinfo/")) + return PROC_NOT_INTERCEPTED; + + return statfs("/tmp", out) < 0 ? -1 : 0; +} + int proc_intercept_stat(const char *path, struct stat *st) { /* Intercept stat for /proc paths emulated via proc_intercept_open. Without diff --git a/src/runtime/procemu.h b/src/runtime/procemu.h index 8e5a8db7..3ab092c4 100644 --- a/src/runtime/procemu.h +++ b/src/runtime/procemu.h @@ -13,6 +13,7 @@ #include #include +#include #include #include #include "core/guest.h" @@ -44,6 +45,16 @@ int proc_intercept_readlink(const char *path, char *buf, size_t bufsiz); */ int proc_intercept_stat(const char *path, struct stat *mac_st); +/* Intercept statfs for /proc paths that would otherwise route through + * proc_intercept_open purely to obtain filesystem-level info, forcing + * allocation of scratch state (e.g. /proc/self/fd's per-open directory of + * placeholder files, one per live guest fd) that statfs() has no use for. + * Returns 0 if the filesystem info was synthesized (mac_st filled), or -2 + * (PROC_NOT_INTERCEPTED) if the path is not intercepted (fall through to the + * normal open/fstatfs/close path). + */ +int proc_intercept_statfs(const char *path, struct statfs *mac_st); + /* Intercept writes to synthetic proc files that need stateful behavior. * Returns 1 if handled (with *written_out set), 0 if not intercepted, or -1 on * error with errno set. diff --git a/src/syscall/fs-stat.c b/src/syscall/fs-stat.c index 2cdbeec2..ddcbe9e4 100644 --- a/src/syscall/fs-stat.c +++ b/src/syscall/fs-stat.c @@ -21,6 +21,7 @@ #include "syscall/internal.h" #include "syscall/path.h" #include "syscall/proc.h" +#include "utils.h" static uint64_t mac_to_linux_dev(dev_t dev) { @@ -355,6 +356,17 @@ int64_t sys_newfstatat(guest_t *g, return 0; } +/* True when path (already translated/normalized by path_translate_at) names + * /proc itself or something under it. Boundary-checked so "/procfoo" does not + * false-positive the way a bare strncmp(path, "/proc", 5) would. + */ +static bool statfs_path_is_proc(const char *path) +{ + if (!path || path[0] != '/') + return false; + return !strncmp(path, "/proc", 5) && (path[5] == '\0' || path[5] == '/'); +} + int64_t sys_statfs(guest_t *g, uint64_t path_gva, uint64_t buf_gva) { char path[LINUX_PATH_MAX]; @@ -368,8 +380,36 @@ int64_t sys_statfs(guest_t *g, uint64_t path_gva, uint64_t buf_gva) return -LINUX_ENOSYS; struct statfs mac_st; - if (statfs(tx.host_path, &mac_st) < 0) + + /* /proc has no host-filesystem counterpart under the sysroot: every entry + * is synthesized on open (see proc_intercept_open), so a raw statfs() on + * the guest path always misses with ENOENT even for "/proc" itself. Try + * the lightweight statfs-specific resolver first (it shortcuts nodes whose + * open path would otherwise allocate scratch state statfs never uses), + * then fall back to the same intercept open/fstatfs/close that + * sys_newfstatat uses via proc_intercept_stat. + */ + if (statfs_path_is_proc(tx.intercept_path)) { + int intercepted = proc_intercept_statfs(tx.intercept_path, &mac_st); + if (intercepted == PROC_NOT_INTERCEPTED) { + int host_fd = proc_intercept_open(g, tx.intercept_path, 0, 0); + if (host_fd == PROC_NOT_INTERCEPTED) { + if (statfs(tx.host_path, &mac_st) < 0) + return linux_errno(); + } else if (host_fd < 0) { + return linux_errno(); + } else { + int rc = fstatfs(host_fd, &mac_st); + close_keep_errno(host_fd); + if (rc < 0) + return linux_errno(); + } + } else if (intercepted < 0) { + return linux_errno(); + } + } else if (statfs(tx.host_path, &mac_st) < 0) { return linux_errno(); + } linux_statfs_t lin_st; translate_statfs(&mac_st, &lin_st); diff --git a/tests/test-proc.c b/tests/test-proc.c index 092f4399..b3b26931 100644 --- a/tests/test-proc.c +++ b/tests/test-proc.c @@ -7,10 +7,11 @@ * * Tests: /proc/self/cmdline, /proc/meminfo, /proc/stat, /proc/version, * /proc/filesystems, /proc/mounts, readlink(/proc/self/exe), + * statfs(/proc, /proc/self/cmdline, /proc/self/fd, /proc/self/fdinfo), * /dev/null, /dev/zero, /dev/urandom * * Syscalls exercised: openat(56), read(63), write(64), readlinkat(78), - * close(57) + * close(57), statfs(43), fstatfs(44) */ #include @@ -21,6 +22,7 @@ #include #include #include +#include #include "test-harness.h" #include "test-util.h" @@ -199,6 +201,85 @@ int main(void) FAIL("opendir failed"); } + /* statfs("/proc") and friends: /proc has no host-filesystem counterpart + * under the sysroot, so this exercises the intercept path (issue #141) + * rather than a raw host statfs() on the guest-supplied string. + */ + TEST("statfs /proc"); + { + struct statfs st; + if (statfs("/proc", &st) < 0) + FAIL("statfs failed"); + else + PASS(); + } + + TEST("statfs /proc/ (trailing slash)"); + { + struct statfs st; + if (statfs("/proc/", &st) < 0) + FAIL("statfs failed"); + else + PASS(); + } + + TEST("statfs /proc/self/cmdline"); + { + struct statfs st; + if (statfs("/proc/self/cmdline", &st) < 0) + FAIL("statfs failed"); + else + PASS(); + } + + TEST("statfs matches fstatfs for /proc/self/cmdline"); + { + struct statfs path_st, fd_st; + int fd = open("/proc/self/cmdline", O_RDONLY); + if (fd < 0) { + FAIL("open failed"); + } else if (statfs("/proc/self/cmdline", &path_st) < 0) { + FAIL("statfs failed"); + } else if (fstatfs(fd, &fd_st) < 0) { + FAIL("fstatfs failed"); + } else { + EXPECT_TRUE(path_st.f_type == fd_st.f_type, + "statfs/fstatfs f_type mismatch"); + } + if (fd >= 0) + close(fd); + } + + /* /proc/self/fd and /proc/self/fdinfo are directories whose *open* path + * allocates a fresh scratch directory with one placeholder file per live + * guest fd (see proc_open_fd_scratch) -- work a plain statfs() has no use + * for. These exercise the statfs-specific bypass (proc_intercept_statfs) + * added alongside issue #141's fix, including enough repeats to exceed + * PROC_SCRATCH_DIRS_MAX (128). The scratch dirs live under the real host + * /tmp, outside anything this guest binary can see, so the loop can only + * confirm statfs() keeps succeeding -- it cannot observe, and so cannot + * fail on, whether the bypass actually avoids allocating those dirs. + */ + TEST("statfs /proc/self/fd"); + { + struct statfs st; + int ok = 1; + for (int i = 0; i < 200 && ok; i++) + if (statfs("/proc/self/fd", &st) < 0) + ok = 0; + EXPECT_TRUE(ok, "statfs failed"); + } + + TEST("statfs /proc/self/fdinfo"); + { + struct statfs st; + int ok = 1; + for (int i = 0; i < 200 && ok; i++) + if (statfs("/proc/self/fdinfo", &st) < 0) + ok = 0; + EXPECT_TRUE(ok, "statfs failed"); + } + TEST("chdir /proc preserves guest cwd"); { char cwd[256];