Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion src/runtime/procemu.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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/<our_pid>[/...] -> /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
Expand Down
11 changes: 11 additions & 0 deletions src/runtime/procemu.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

#include <stddef.h>
#include <stdint.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include "core/guest.h"
Expand Down Expand Up @@ -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.
Expand Down
42 changes: 41 additions & 1 deletion src/syscall/fs-stat.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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];
Expand All @@ -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);
Expand Down
83 changes: 82 additions & 1 deletion tests/test-proc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 <stdlib.h>
Expand All @@ -21,6 +22,7 @@
#include <dirent.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/statfs.h>

#include "test-harness.h"
#include "test-util.h"
Expand Down Expand Up @@ -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");
Comment thread
Max042004 marked this conversation as resolved.
{
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];
Expand Down
Loading