Skip to content

Resolve /dev/shm host paths in one place#228

Open
henrybear327 wants to merge 1 commit into
sysprog21:mainfrom
henrybear327:fix/dev-shm-path-redirect
Open

Resolve /dev/shm host paths in one place#228
henrybear327 wants to merge 1 commit into
sysprog21:mainfrom
henrybear327:fix/dev-shm-path-redirect

Conversation

@henrybear327

@henrybear327 henrybear327 commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

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 failing to run all tests due to the following:

  • 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.


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.

  • Bug Fixes
    • Redirect /dev/shm/ in path_translate_at via the validated resolver and a new shm_redirect flag, so chmod, chown, truncate, utimensat, rename, link/symlink, mknod, readlink, mkdir, statfs, and xattr ops hit the same backing path as open/unlink.
    • Enforce nofollow on shm paths to prevent host escapes: lstat in stat, AT_SYMLINK_NOFOLLOW on *at and xattr, O_NOFOLLOW + ftruncate for truncate, and clear AT_SYMLINK_FOLLOW for linkat.
    • Introduce path_tx_dirfd to select AT_FDCWD for absolute redirects and update *at calls; remove the unlinkat special-case.
    • Add test-dev-shm-paths to cover open→chmod, metadata ops, rename/link/unlink, statfs, symlink containment, and name-shape gate; enable in tests/manifest.txt.

Written for commit 8cee1c5. Summary will update on new commits.

Review in cubic

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/syscall/fs.c
Comment on lines +2377 to +2378
if (faccessat(path_tx_dirfd(&tx, &dir_ref), tx.host_path, mode, mac_flags) <
0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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
jserv requested a review from Max042004 July 19, 2026 06:11

@jserv jserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/syscall/path.c
sizeof(tx->host_buf)) < 0)
return -1;
tx->host_path = tx->host_buf;
tx->shm_redirect = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/syscall/fs.c

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) <

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/syscall/fs.c
* O_NOFOLLOW + ftruncate (see dev_shm_resolve_path).
*/
if (tx.shm_redirect) {
int fd = open(tx.host_path, O_WRONLY | O_NOFOLLOW);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/runtime/procemu.c
* __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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants