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];