Resolve /dev/shm host paths in one place#228
Conversation
macOS has no /dev/shm, so elfuse backs POSIX shared memory with a
per-UID host directory, /tmp/elfuse-shm-<uid>/<name>, via the validated
resolver proc_dev_shm_resolve (flat leaf only; "..", embedded '/', and
empty names rejected). Only three syscalls performed that redirect by
hand: open, stat, and unlinkat. Every other path syscall fell through
path_translate_at, which prepends the sysroot instead, so the same
guest path resolved two different ways:
/dev/shm/foo
open -> /tmp/elfuse-shm-1000/foo (backing dir, created)
chmod -> <sysroot>/dev/shm/foo (nothing here -> ENOENT)
LTP's library startup (lib/tst_test.c setup_ipc) is exactly this: an
open(O_CREAT|O_EXCL) on a /dev/shm path followed by SAFE_CHMOD of the
same path. The open landed in the backing dir and succeeded; the chmod
resolved the sysroot path, hit ENOENT, and aborted the process in
common setup, so all 24 conformance tests failed before any test body
ran.
The fix is to do the redirect once, in path_translate_at, through the
same resolver and a new shm_redirect flag, so chmod, chown, truncate,
utimensat, rename, link, symlink, mknod, readlink, mkdir, statfs, and
the xattr family all inherit the backing path from one choke point.
The backing path is absolute and POSIX ignores dirfd for absolute paths,
so path_tx_dirfd() selects AT_FDCWD and the hand-rolled unlinkat rewrite
folds away.
There was a problem hiding this comment.
1 issue found across 8 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/syscall/fs.c">
<violation number="1" location="src/syscall/fs.c:2377">
P1: `faccessat2` on a `/dev/shm` symlink follows its host target, allowing the guest to probe permissions/existence outside the backing directory. Add `AT_SYMLINK_NOFOLLOW` for `tx.shm_redirect`, matching the redirect containment invariant.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| if (faccessat(path_tx_dirfd(&tx, &dir_ref), tx.host_path, mode, mac_flags) < | ||
| 0) { |
There was a problem hiding this comment.
P1: faccessat2 on a /dev/shm symlink follows its host target, allowing the guest to probe permissions/existence outside the backing directory. Add AT_SYMLINK_NOFOLLOW for tx.shm_redirect, matching the redirect containment invariant.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/syscall/fs.c, line 2377:
<comment>`faccessat2` on a `/dev/shm` symlink follows its host target, allowing the guest to probe permissions/existence outside the backing directory. Add `AT_SYMLINK_NOFOLLOW` for `tx.shm_redirect`, matching the redirect containment invariant.</comment>
<file context>
@@ -2374,7 +2374,8 @@ int64_t sys_faccessat(guest_t *g,
int mac_flags = translate_faccessat_flags(flags);
- if (faccessat(dir_ref.fd, tx.host_path, mode, mac_flags) < 0) {
+ if (faccessat(path_tx_dirfd(&tx, &dir_ref), tx.host_path, mode, mac_flags) <
+ 0) {
host_fd_ref_close(&dir_ref);
</file context>
| if (faccessat(path_tx_dirfd(&tx, &dir_ref), tx.host_path, mode, mac_flags) < | |
| 0) { | |
| if (tx.shm_redirect) | |
| mac_flags |= AT_SYMLINK_NOFOLLOW; | |
| if (faccessat(path_tx_dirfd(&tx, &dir_ref), tx.host_path, mode, mac_flags) < | |
| 0) { |
jserv
left a comment
There was a problem hiding this comment.
Centralizing the /dev/shm redirect in path_translate_at is the right call, but the shared primitive now reaches follow-capable callers that were never taught to force nofollow. Inline notes below. Test gap: tests/test-dev-shm-paths.c exercises the symlink-not-followed case only for the metadata ops, not for statfs/chdir/execve on a symlink leaf.
| sizeof(tx->host_buf)) < 0) | ||
| return -1; | ||
| tx->host_path = tx->host_buf; | ||
| tx->shm_redirect = true; |
There was a problem hiding this comment.
Redirecting here also reaches follow-capable callers outside the nofollow-forced metadata set: statfs() (fs-stat.c:434), chdir() (fs.c:1746/1752), execve()/interp resolution (exec.c:175,598), and bootstrap interp (bootstrap.c:214). Each dereferences a symlink leaf onto the host, escaping the backing dir. Either carry shm_redirect into those calls and force nofollow (fstatfs/fchdir via an O_NOFOLLOW open, O_NOFOLLOW on exec), or reject a symlink shm leaf up front.
|
|
||
| int mac_flags = translate_faccessat_flags(flags); | ||
| if (faccessat(dir_ref.fd, tx.host_path, mode, mac_flags) < 0) { | ||
| if (faccessat(path_tx_dirfd(&tx, &dir_ref), tx.host_path, mode, mac_flags) < |
There was a problem hiding this comment.
faccessat doesn't set AT_SYMLINK_NOFOLLOW on shm_redirect, unlike fchmodat/fchownat/utimensat. Add if (tx.shm_redirect) mac_flags |= AT_SYMLINK_NOFOLLOW; so the leaf stays non-followed in the ENOENT-at-stat-time race window.
| * O_NOFOLLOW + ftruncate (see dev_shm_resolve_path). | ||
| */ | ||
| if (tx.shm_redirect) { | ||
| int fd = open(tx.host_path, O_WRONLY | O_NOFOLLOW); |
There was a problem hiding this comment.
O_NOFOLLOW guards symlinks but not FIFOs: mknod a shm FIFO, then truncate it, and this open(O_WRONLY) blocks the vCPU thread until a reader appears — Linux returns EINVAL immediately. Add O_NONBLOCK (fails fast with ENXIO), plus O_CLOEXEC on the short-lived fd.
| * __shm_get_name (posix/shm-directory.c) strips the leading slash and rejects | ||
| * an empty name or any embedded '/' with EINVAL, so a real POSIX shm name is | ||
| * always a leaf. This helper enforces the same shape, additionally rejects | ||
| * "..", and reports EACCES (ENAMETOOLONG when the host path overflows). |
There was a problem hiding this comment.
The .. gate below uses strstr(guest_suffix, "..") (line 710), which rejects any name containing .., so valid flat names like a..b now fail with EACCES across every redirected syscall. If the goal is only traversal prevention, compare the whole component: strcmp(guest_suffix, "..") == 0.
macOS has no
/dev/shm, so elfuse backs POSIX shared memory with a per-UID host directory,/tmp/elfuse-shm-<uid>/<name>, via the validated resolverproc_dev_shm_resolve(flat leaf only; "..", embedded '/', and empty names rejected).Only three syscalls performed that redirect by hand: open, stat, and unlinkat. Every other path syscall fell through path_translate_at, which prepends the sysroot instead, so the same guest path resolved two different ways:
LTP's library startup (lib/tst_test.c setup_ipc) is failing to run all tests due to the following:
The fix is to do the redirect once, in path_translate_at, through the same resolver and a new shm_redirect flag, so chmod, chown, truncate, utimensat, rename, link, symlink, mknod, readlink, mkdir, statfs, and the xattr family all inherit the backing path from one choke point.
The backing path is absolute and POSIX ignores dirfd for absolute paths, so path_tx_dirfd() selects AT_FDCWD and the hand-rolled unlinkat rewrite folds away.
Summary by cubic
Centralized /dev/shm path resolution so all path syscalls use the same per-UID backing directory on macOS. Fixes LTP setup_ipc failing after open(O_CREAT) followed by chmod on the same shm path.
Written for commit 8cee1c5. Summary will update on new commits.