Skip to content

Commit 451b77e

Browse files
committed
Stub /sys/devices/system/cpu and harden checks
Java GC, Go scheduler init, and libnuma probe /sys/devices/system/cpu/{online,possible,present} plus per-CPU dirs to size thread pools. macOS has no /sys, so the lack of these files made those probes fall back to suboptimal heuristics or fail outright. ensure_syscpu_dir lazily builds /tmp/elfuse-syscpu-XXXXXX/ on first access, populated with online/possible/present cpumask range files (sysconf(_SC_NPROCESSORS_ONLN) gives "0" for one CPU, "0-N-1" for N) and one empty cpuN/ directory per host CPU. The cache/topology subtrees stay empty so deeper queries return ENOENT until a real consumer asks. Population is one-shot: the host CPU count does not change at runtime so refresh is unnecessary. Hardening guards on the open and stat paths: - syscpu_open_is_readonly rejects non-RDONLY accmode plus O_CREAT and O_TRUNC with EACCES so the stub stays read-only as a real sysfs would, covering both the bare cpu root and child paths. - syscpu_suffix_safe rejects any '..' component before path join so a guest open of /sys/devices/system/cpu/../../etc/passwd cannot pivot the lstat/open onto an arbitrary host file. - ensure_syscpu_dir tears down the partial scratch dir on any write_file/mkdir failure instead of caching a half-built state with syscpu_dir_ok=true. - A getpid()-vs-syscpu_owner_pid guard in syscpu_dir_cleanup keeps clone(CLONE_VM) children from rmdir'ing the parent's still-active scratch tree at exit. - path_prefix_match in path.c tightens the prefix test so /sys/devices/system/cpufoo no longer falls into the intercept layer. - syscpu_classify centralizes SYSFS_CPU prefix handling between proc_intercept_open and proc_intercept_stat as one source of truth. While auditing access(2) for the new stub, the previous "intercept matched, return 0" shortcut leaked false positives: a guest probing W_OK on an intercepted path received 0 even when no W bit was set in the synthesized stat. path_check_intercept_access now does proper POSIX mode-bit checking against the stat result, with standard owner/group/other selection plus a CAP_DAC_OVERRIDE-style root branch that grants RW always and X if any X bit is set. The synthetic stat fillers now populate st_uid/st_gid from proc_get_uid/proc_get_gid so the owner branch matches. faccessat SYS 48 dispatch passed x3 to sys_faccessat even though Linux's 3-arg faccessat has no flags parameter; x3 carried whatever garbage was in the caller's register state, and translate_faccessat_flags would set AT_EACCESS or AT_SYMLINK_NOFOLLOW semi-randomly. SYS 48 now forces flags=0; SYS 439 (faccessat2) keeps x3 as before.
1 parent 1e00808 commit 451b77e

8 files changed

Lines changed: 703 additions & 30 deletions

File tree

src/runtime/procemu.c

Lines changed: 363 additions & 0 deletions
Large diffs are not rendered by default.

src/syscall/fs.c

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,15 +1410,16 @@ int64_t sys_faccessat(guest_t *g,
14101410
if (host_dirfd_ref_open(dirfd, &dir_ref) < 0)
14111411
return -LINUX_EBADF;
14121412

1413-
/* Check /proc paths first since macOS has no /proc filesystem, so
1414-
* access("/proc/self/stat", R_OK) etc. must be intercepted.
1415-
* If proc_intercept_stat succeeds, the path is a known emulated
1416-
* entry and the code reports it as accessible.
1413+
/* Check intercepted stat paths first since macOS has no /proc filesystem
1414+
* and the sysfs CPU tree is synthetic. Access must reflect the synthetic
1415+
* mode bits, not just path existence.
14171416
*/
1418-
struct stat dummy_st;
1417+
struct stat intercepted_st;
14191418
if (path_might_use_stat_intercept(access_path) &&
1420-
proc_intercept_stat(access_path, &dummy_st) == 0) {
1419+
proc_intercept_stat(access_path, &intercepted_st) == 0) {
14211420
host_fd_ref_close(&dir_ref);
1421+
if (path_check_intercept_access(&intercepted_st, mode, flags) < 0)
1422+
return linux_errno();
14221423
return 0;
14231424
}
14241425

src/syscall/path.c

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,59 +26,125 @@
2626

2727
#define PROC_PATH_COMPONENTS_MAX (LINUX_PATH_MAX / 2)
2828

29-
int path_might_use_open_intercept(const char *path)
29+
/* True when path equals prefix exactly, or extends it with '/'. Avoids the
30+
* surprise where "/sys/devices/system/cpufoo" would match a bare strncmp on
31+
* "/sys/devices/system/cpu" and pull an unrelated path through the intercept
32+
* layer.
33+
*/
34+
static bool path_prefix_match(const char *path, const char *prefix, size_t plen)
35+
{
36+
if (strncmp(path, prefix, plen) != 0)
37+
return false;
38+
return path[plen] == '\0' || path[plen] == '/';
39+
}
40+
41+
#define SYSFS_CPU_PREFIX "/sys/devices/system/cpu"
42+
43+
bool path_might_use_open_intercept(const char *path)
3044
{
3145
if (!path || path[0] != '/')
32-
return 0;
46+
return false;
3347

3448
if (!strncmp(path, "/proc", 5))
35-
return 1;
49+
return true;
3650
if (!strncmp(path, "/dev", 4))
37-
return 1;
51+
return true;
52+
if (path_prefix_match(path, SYSFS_CPU_PREFIX, sizeof(SYSFS_CPU_PREFIX) - 1))
53+
return true;
3854
if (!strcmp(path, "/etc/mtab") || !strcmp(path, "/etc/passwd") ||
3955
!strcmp(path, "/etc/group"))
40-
return 1;
56+
return true;
4157
if (!strcmp(path, "/var/run/utmp") || !strcmp(path, "/run/utmp"))
42-
return 1;
58+
return true;
4359

44-
return 0;
60+
return false;
4561
}
4662

47-
int path_might_use_stat_intercept(const char *path)
63+
bool path_might_use_stat_intercept(const char *path)
4864
{
4965
if (!path || path[0] != '/')
50-
return 0;
66+
return false;
5167

5268
if (!strncmp(path, "/proc", 5))
53-
return 1;
69+
return true;
5470
if (!strncmp(path, "/dev/shm", 8))
55-
return 1;
71+
return true;
72+
if (path_prefix_match(path, SYSFS_CPU_PREFIX, sizeof(SYSFS_CPU_PREFIX) - 1))
73+
return true;
5674

57-
return 0;
75+
return false;
5876
}
5977

60-
static int path_next_component(const char **pathp,
61-
const char **comp,
62-
size_t *len)
78+
int path_check_intercept_access(const struct stat *st, int mode, int flags)
79+
{
80+
if ((mode & ~(F_OK | R_OK | W_OK | X_OK)) != 0) {
81+
errno = EINVAL;
82+
return -1;
83+
}
84+
if (mode == F_OK)
85+
return 0;
86+
87+
mode_t granted = 0;
88+
uint32_t uid =
89+
(flags & LINUX_AT_EACCESS) ? proc_get_euid() : proc_get_uid();
90+
uint32_t gid =
91+
(flags & LINUX_AT_EACCESS) ? proc_get_egid() : proc_get_gid();
92+
93+
if (uid == 0) {
94+
/* CAP_DAC_OVERRIDE: root reads and writes any file regardless of mode
95+
* bits. Execute still requires at least one x-bit set so non-executable
96+
* files cannot be run as root. Matches Linux generic_permission() in
97+
* fs/namei.c.
98+
*/
99+
granted |= R_OK | W_OK;
100+
if (st->st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
101+
granted |= X_OK;
102+
} else {
103+
mode_t bits;
104+
if (uid == st->st_uid)
105+
bits = (st->st_mode >> 6) & 7;
106+
else if (gid == st->st_gid)
107+
bits = (st->st_mode >> 3) & 7;
108+
else
109+
bits = st->st_mode & 7;
110+
111+
if (bits & 4)
112+
granted |= R_OK;
113+
if (bits & 2)
114+
granted |= W_OK;
115+
if (bits & 1)
116+
granted |= X_OK;
117+
}
118+
119+
if ((mode & granted) == mode)
120+
return 0;
121+
122+
errno = EACCES;
123+
return -1;
124+
}
125+
126+
static bool path_next_component(const char **pathp,
127+
const char **comp,
128+
size_t *len)
63129
{
64130
const char *p = *pathp;
65131

66132
while (*p == '/')
67133
p++;
68134
if (*p == '\0') {
69135
*pathp = p;
70-
return 0;
136+
return false;
71137
}
72138

73139
*comp = p;
74140
while (*p != '\0' && *p != '/')
75141
p++;
76142
*len = (size_t) (p - *comp);
77143
*pathp = p;
78-
return 1;
144+
return true;
79145
}
80146

81-
static int path_component_is_dot(const char *comp, size_t len)
147+
static bool path_component_is_dot(const char *comp, size_t len)
82148
{
83149
return len == 1 && comp[0] == '.';
84150
}

src/syscall/path.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
#include <stdbool.h>
1010
#include <stddef.h>
1111
#include <stdint.h>
12+
#include <sys/stat.h>
1213

1314
#include "syscall/internal.h"
1415

15-
int path_might_use_open_intercept(const char *path);
16-
int path_might_use_stat_intercept(const char *path);
16+
bool path_might_use_open_intercept(const char *path);
17+
bool path_might_use_stat_intercept(const char *path);
18+
int path_check_intercept_access(const struct stat *st, int mode, int flags);
1719
int resolve_proc_at_path(guest_fd_t dirfd,
1820
const char *path,
1921
char *out,

src/syscall/syscall.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,11 @@ SC_FORWARD(sc_fchmodat2, sys_fchmodat(g, (int) x0, x1, (uint32_t) x2, (int) x3
213213
SC_FORWARD(sc_fchownat, sys_fchownat(g, (int) x0, x1, (uint32_t) x2, (uint32_t) x3, (int) x4))
214214
SC_FORWARD(sc_fchown, sys_fchown((int) x0, (uint32_t) x1, (uint32_t) x2))
215215
SC_FORWARD(sc_utimensat, sys_utimensat(g, (int) x0, x1, x2, (int) x3))
216-
SC_FORWARD(sc_faccessat, sys_faccessat(g, (int) x0, x1, (int) x2, (int) x3))
216+
/* Linux faccessat (SYS 48) is 3-arg: dirfd, path, mode.
217+
* The flags parameter was added in faccessat2 (SYS 439).
218+
* x3 contains garbage from the caller's register state.
219+
*/
220+
SC_FORWARD(sc_faccessat, sys_faccessat(g, (int) x0, x1, (int) x2, 0))
217221
SC_FORWARD(sc_faccessat2, sys_faccessat(g, (int) x0, x1, (int) x2, (int) x3))
218222
SC_FORWARD(sc_ftruncate, sys_ftruncate((int) x0, (int64_t) x1))
219223
SC_FORWARD(sc_truncate, sys_truncate(g, x0, (int64_t) x1))

tests/manifest.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ test-large-io-boundary
5757

5858
[section] /proc and /dev emulation tests
5959
test-proc
60+
test-sysfs-cpu
6061

6162
[section] Network tests
6263
test-net

tests/test-matrix.sh

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,12 @@ run_elfuse_sysroot()
180180
# Tests that either hang under qemu-system-aarch64 on Apple Silicon
181181
# (raw clone / PI futex / massive thread+mmap stress) or currently diverge
182182
# from the Alpine linux-virt reference kernel on the deprecated oom_adj
183-
# procfs compatibility path exercised by test-io-opt. They still run in
184-
# elfuse-aarch64 mode and in `make check`; the qemu reference run skips them.
185-
QEMU_SKIP="test-thread test-stress test-futex-pi test-io-opt"
183+
# procfs compatibility path exercised by test-io-opt. test-sysfs-cpu asserts
184+
# the elfuse stub contract (cache/topology subtree empty, possible == online,
185+
# cpuN count == online count) which a real kernel does not honor. All listed
186+
# tests still run in elfuse-aarch64 mode and in `make check`; the qemu
187+
# reference run skips them.
188+
QEMU_SKIP="test-thread test-stress test-futex-pi test-io-opt test-sysfs-cpu"
186189

187190
is_qemu_skipped()
188191
{
@@ -355,6 +358,7 @@ run_unit_tests()
355358

356359
printf "\n/proc and /dev\n"
357360
test_check "$runner" "test-proc" "0 failed" "$bindir/test-proc"
361+
test_check "$runner" "test-sysfs-cpu" "0 failed" "$bindir/test-sysfs-cpu"
358362

359363
printf "\nNetwork\n"
360364
test_check "$runner" "test-net" "0 failed" "$bindir/test-net"

0 commit comments

Comments
 (0)