From a93fac7753493e2a4a3e1f120ee4909b47cf6b04 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Sat, 18 Jul 2026 14:06:31 +0200 Subject: [PATCH] Resolve /dev/shm host paths in one place macOS has no /dev/shm, so elfuse backs POSIX shared memory with a per-UID host directory, /tmp/elfuse-shm-/, 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 -> /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. --- src/runtime/procemu.c | 33 ++++- src/syscall/fs-stat.c | 3 +- src/syscall/fs-xattr.c | 8 +- src/syscall/fs.c | 106 ++++++++------ src/syscall/path.c | 22 +++ src/syscall/path.h | 17 +++ tests/manifest.txt | 1 + tests/test-dev-shm-paths.c | 276 +++++++++++++++++++++++++++++++++++++ 8 files changed, 413 insertions(+), 53 deletions(-) create mode 100644 tests/test-dev-shm-paths.c diff --git a/src/runtime/procemu.c b/src/runtime/procemu.c index 77a4a5e5..7504196b 100644 --- a/src/runtime/procemu.c +++ b/src/runtime/procemu.c @@ -683,11 +683,27 @@ static int proc_parse_fd_index(const char *path, return (int) n; } -/* Resolve a /dev/shm/ guest path to a host path inside the per-UID shm - * dir. Rejects empty, traversing, or compound suffixes with EACCES; reports - * ENAMETOOLONG when the host path overflows. The same validation runs in - * proc_intercept_open and proc_intercept_stat, so the helper is one source of - * truth for the security gate. +/* Resolve a /dev/shm/ guest path to a host path inside the per-UID + * shm backing dir. The suffix must be a single flat component: glibc's + * __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). + * + * The backing store is a host directory, not an in-namespace tmpfs, so + * following a symlink planted at a shm leaf would escape onto the host. + * glibc's shm_open (sysdeps/posix/shm_open.c) already opens shm objects with + * O_NOFOLLOW | O_CLOEXEC, so a symlink at a shm name is never a legitimate + * object. Every caller must act on the leaf without following it: O_NOFOLLOW + * on open and truncate, AT_SYMLINK_NOFOLLOW on the *at() metadata calls, + * XATTR_NOFOLLOW on xattr, lstat for stat, and linkat without + * AT_SYMLINK_FOLLOW. On real Linux the same chmod or stat would follow the + * link, but its target resolves inside the guest namespace; here it resolves + * on the host, so nofollow is what keeps the redirect contained. + * + * This helper is the single source of truth for the name gate and that + * invariant. Callers: proc_intercept_open, proc_intercept_stat, and + * path_translate_at (which sets path_translation_t.shm_redirect). */ static int dev_shm_resolve_path(const char *guest_suffix, char *host_path, @@ -3497,12 +3513,15 @@ int proc_intercept_stat(const char *path, struct stat *st) path); /* sticky bit, like real /dev/shm */ return 0; } - /* /dev/shm/ files: check the host temp dir */ + /* /dev/shm/ files: check the host backing dir, and lstat rather than + * stat so a planted symlink leaf is never followed (see + * dev_shm_resolve_path). + */ if (!strncmp(path, "/dev/shm/", 9)) { char host_path[512]; if (dev_shm_resolve_path(path + 9, host_path, sizeof(host_path)) < 0) return -1; - return stat(host_path, st); + return lstat(host_path, st); } /* /dev/pts directory and /dev/pts/N slave entries. glibc ptsname(3) stats diff --git a/src/syscall/fs-stat.c b/src/syscall/fs-stat.c index 9e10c477..f4ae75b8 100644 --- a/src/syscall/fs-stat.c +++ b/src/syscall/fs-stat.c @@ -270,7 +270,8 @@ static int64_t stat_at_path(guest_t *g, } if (intercepted == PROC_NOT_INTERCEPTED) { int mac_flags = translate_at_flags(flags); - if (fstatat(dir_ref.fd, tx.host_path, mac_st, mac_flags) < 0) { + if (fstatat(path_tx_dirfd(&tx, &dir_ref), tx.host_path, mac_st, + mac_flags) < 0) { rc = linux_errno(); goto done; } diff --git a/src/syscall/fs-xattr.c b/src/syscall/fs-xattr.c index fe70fecd..97821734 100644 --- a/src/syscall/fs-xattr.c +++ b/src/syscall/fs-xattr.c @@ -86,7 +86,7 @@ int64_t sys_getxattr(guest_t *g, if (tx.fuse_path) return -LINUX_ENOSYS; - int opts = nofollow ? XATTR_NOFOLLOW : 0; + int opts = (nofollow || tx.shm_redirect) ? XATTR_NOFOLLOW : 0; if (size == 0) { ssize_t ret = getxattr(tx.host_path, name, NULL, 0, 0, opts); @@ -134,7 +134,7 @@ int64_t sys_setxattr(guest_t *g, return -LINUX_EFAULT; } - int opts = nofollow ? XATTR_NOFOLLOW : 0; + int opts = (nofollow || tx.shm_redirect) ? XATTR_NOFOLLOW : 0; err = xattr_translate_flags(flags, &opts); if (err < 0) { free(buf); @@ -163,7 +163,7 @@ int64_t sys_listxattr(guest_t *g, if (tx.fuse_path) return -LINUX_ENOSYS; - int opts = nofollow ? XATTR_NOFOLLOW : 0; + int opts = (nofollow || tx.shm_redirect) ? XATTR_NOFOLLOW : 0; if (size == 0) { ssize_t ret = listxattr(tx.host_path, NULL, 0, opts); @@ -199,7 +199,7 @@ int64_t sys_removexattr(guest_t *g, if (tx.fuse_path) return -LINUX_ENOSYS; - int opts = nofollow ? XATTR_NOFOLLOW : 0; + int opts = (nofollow || tx.shm_redirect) ? XATTR_NOFOLLOW : 0; int ret = removexattr(tx.host_path, name, opts); return ret < 0 ? linux_errno() : 0; } diff --git a/src/syscall/fs.c b/src/syscall/fs.c index 0a512c92..d83e0662 100644 --- a/src/syscall/fs.c +++ b/src/syscall/fs.c @@ -1909,7 +1909,8 @@ int64_t sys_readlinkat(guest_t *g, return -LINUX_EBADF; /* Apply sysroot redirect for absolute paths */ - ssize_t len = readlinkat(dir_ref.fd, tx.host_path, link, sizeof(link) - 1); + ssize_t len = readlinkat(path_tx_dirfd(&tx, &dir_ref), tx.host_path, link, + sizeof(link) - 1); host_fd_ref_close(&dir_ref); if (len < 0) return linux_errno(); @@ -1947,31 +1948,19 @@ int64_t sys_unlinkat(guest_t *g, int dirfd, uint64_t path_gva, int flags) if (host_dirfd_ref_open(dirfd, &dir_ref) < 0) return -LINUX_EBADF; - /* Rewrite /dev/shm/ to the host temp directory so shm_unlink works */ - const char *unlink_path; - char shm_host[LINUX_PATH_MAX]; - if (!strncmp(tx.guest_path, "/dev/shm/", 9)) { - if (proc_dev_shm_resolve(tx.guest_path + 9, shm_host, - sizeof(shm_host)) < 0) { - host_fd_ref_close(&dir_ref); - return linux_errno(); - } - unlink_path = shm_host; - host_fd_ref_close(&dir_ref); - dir_ref.fd = AT_FDCWD; - dir_ref.owned = 0; - } else { - unlink_path = tx.host_path; - } + /* path_translate_at rewrites /dev/shm/ to the absolute backing + * path, so shm_unlink works; path_tx_dirfd drops the guest dirfd there. + */ + host_fd_t unlink_dirfd = path_tx_dirfd(&tx, &dir_ref); struct stat removed_st; bool clear_removed_overlay = - fstatat(dir_ref.fd, unlink_path, &removed_st, AT_SYMLINK_NOFOLLOW) == + fstatat(unlink_dirfd, tx.host_path, &removed_st, AT_SYMLINK_NOFOLLOW) == 0 && (removed_st.st_nlink <= 1 || (flags & LINUX_AT_REMOVEDIR)); int host_flags = translate_at_flags(flags); - if (unlinkat(dir_ref.fd, unlink_path, host_flags) < 0) { + if (unlinkat(unlink_dirfd, tx.host_path, host_flags) < 0) { host_fd_ref_close(&dir_ref); return linux_errno(); } @@ -2006,7 +1995,8 @@ int64_t sys_mkdirat(guest_t *g, int dirfd, uint64_t path_gva, int mode) if (host_dirfd_ref_open(dirfd, &dir_ref) < 0) return -LINUX_EBADF; - if (mkdirat(dir_ref.fd, tx.host_path, (mode_t) mode) < 0) { + if (mkdirat(path_tx_dirfd(&tx, &dir_ref), tx.host_path, (mode_t) mode) < + 0) { host_fd_ref_close(&dir_ref); return linux_errno(); } @@ -2065,6 +2055,8 @@ int64_t sys_renameat2(guest_t *g, host_fd_ref_close(&olddir_ref); return -LINUX_EBADF; } + host_fd_t old_host_dirfd = path_tx_dirfd(&old_tx, &olddir_ref); + host_fd_t new_host_dirfd = path_tx_dirfd(&new_tx, &newdir_ref); /* Apply sysroot resolution for absolute paths RENAME_NOREPLACE: fail if * destination exists. macOS renamex_np supports RENAME_EXCL for the same @@ -2085,14 +2077,14 @@ int64_t sys_renameat2(guest_t *g, * requirement. This path still cannot handle directories because * hardlinking directories is not allowed. */ - if (linkat(olddir_ref.fd, old_tx.host_path, newdir_ref.fd, + if (linkat(old_host_dirfd, old_tx.host_path, new_host_dirfd, new_tx.host_path, 0) < 0) { return close_dir_refs_result(&olddir_ref, &newdir_ref, linux_errno()); } - if (unlinkat(olddir_ref.fd, old_tx.host_path, 0) < 0) { + if (unlinkat(old_host_dirfd, old_tx.host_path, 0) < 0) { int err = errno; - (void) unlinkat(newdir_ref.fd, new_tx.host_path, 0); + (void) unlinkat(new_host_dirfd, new_tx.host_path, 0); errno = err; return close_dir_refs_result(&olddir_ref, &newdir_ref, linux_errno()); @@ -2117,11 +2109,11 @@ int64_t sys_renameat2(guest_t *g, } struct stat old_st; - bool have_old_st = fstatat(olddir_ref.fd, old_tx.host_path, &old_st, + bool have_old_st = fstatat(old_host_dirfd, old_tx.host_path, &old_st, AT_SYMLINK_NOFOLLOW) == 0; struct stat overwritten_st; bool clear_overwritten_overlay = - fstatat(newdir_ref.fd, new_tx.host_path, &overwritten_st, + fstatat(new_host_dirfd, new_tx.host_path, &overwritten_st, AT_SYMLINK_NOFOLLOW) == 0 && stat_identity_will_disappear(&overwritten_st) && (!have_old_st || !same_stat_identity(&old_st, &overwritten_st)); @@ -2136,7 +2128,7 @@ int64_t sys_renameat2(guest_t *g, return close_dir_refs_result(&olddir_ref, &newdir_ref, 0); } - if (renameat(olddir_ref.fd, old_tx.host_path, newdir_ref.fd, + if (renameat(old_host_dirfd, old_tx.host_path, new_host_dirfd, new_tx.host_path) < 0) { return close_dir_refs_result(&olddir_ref, &newdir_ref, linux_errno()); } @@ -2166,7 +2158,8 @@ int64_t sys_mknodat(guest_t *g, int dirfd, uint64_t path_gva, int mode, int dev) * nodes need root */ if (S_ISFIFO(mode)) { - if (mkfifoat(dir_ref.fd, tx.host_path, mode & 0777) < 0) { + if (mkfifoat(path_tx_dirfd(&tx, &dir_ref), tx.host_path, mode & 0777) < + 0) { host_fd_ref_close(&dir_ref); return linux_errno(); } @@ -2176,8 +2169,8 @@ int64_t sys_mknodat(guest_t *g, int dirfd, uint64_t path_gva, int mode, int dev) /* Regular files: create an empty file */ if (S_ISREG(mode) || (mode & S_IFMT) == 0) { - int fd = openat(dir_ref.fd, tx.host_path, O_CREAT | O_WRONLY | O_EXCL, - mode & 0777); + int fd = openat(path_tx_dirfd(&tx, &dir_ref), tx.host_path, + O_CREAT | O_WRONLY | O_EXCL, mode & 0777); host_fd_ref_close(&dir_ref); if (fd < 0) return linux_errno(); @@ -2212,7 +2205,7 @@ int64_t sys_symlinkat(guest_t *g, return -LINUX_EBADF; /* Resolve linkpath (the new symlink location) through sysroot */ - if (symlinkat(target, dir_ref.fd, tx.host_path) < 0) { + if (symlinkat(target, path_tx_dirfd(&tx, &dir_ref), tx.host_path) < 0) { host_fd_ref_close(&dir_ref); return linux_errno(); } @@ -2258,11 +2251,18 @@ int64_t sys_linkat(guest_t *g, host_fd_ref_close(&olddir_ref); return -LINUX_EBADF; } + host_fd_t old_host_dirfd = path_tx_dirfd(&old_tx, &olddir_ref); + host_fd_t new_host_dirfd = path_tx_dirfd(&new_tx, &newdir_ref); /* Resolve both paths through sysroot */ int mac_flags = translate_at_flags(flags); - if (linkat(olddir_ref.fd, old_tx.host_path, newdir_ref.fd, new_tx.host_path, - mac_flags) < 0) { + /* Clear AT_SYMLINK_FOLLOW so a shm symlink is hard-linked as the leaf + * itself, never dereferenced to its host target (see dev_shm_resolve_path). + */ + if (old_tx.shm_redirect) + mac_flags &= ~AT_SYMLINK_FOLLOW; + if (linkat(old_host_dirfd, old_tx.host_path, new_host_dirfd, + new_tx.host_path, mac_flags) < 0) { /* Darwin's linkat(2) man page: without AT_SYMLINK_FOLLOW, hard-linking * a symlink itself (rather than its target) "may result in some file * systems returning an error" -- reproduced here as ENOTSUP on @@ -2283,10 +2283,10 @@ int64_t sys_linkat(guest_t *g, struct stat old_st; char target[LINUX_PATH_MAX]; ssize_t target_len; - if (fstatat(olddir_ref.fd, old_tx.host_path, &old_st, + if (fstatat(old_host_dirfd, old_tx.host_path, &old_st, AT_SYMLINK_NOFOLLOW) < 0 || !S_ISLNK(old_st.st_mode) || - (target_len = readlinkat(olddir_ref.fd, old_tx.host_path, target, + (target_len = readlinkat(old_host_dirfd, old_tx.host_path, target, sizeof(target) - 1)) < 0) { host_fd_ref_close(&olddir_ref); host_fd_ref_close(&newdir_ref); @@ -2294,7 +2294,7 @@ int64_t sys_linkat(guest_t *g, } target[target_len] = '\0'; - if (symlinkat(target, newdir_ref.fd, new_tx.host_path) < 0) { + if (symlinkat(target, new_host_dirfd, new_tx.host_path) < 0) { host_fd_ref_close(&olddir_ref); host_fd_ref_close(&newdir_ref); return linux_errno(); @@ -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); return linux_errno(); } @@ -2437,6 +2438,21 @@ int64_t sys_truncate(guest_t *g, uint64_t path_gva, int64_t length) if (rc != INT64_MIN) return rc; + /* truncate(2) has no nofollow variant, so reach a shm leaf via open + * O_NOFOLLOW + ftruncate (see dev_shm_resolve_path). + */ + if (tx.shm_redirect) { + int fd = open(tx.host_path, O_WRONLY | O_NOFOLLOW); + if (fd < 0) + return linux_errno(); + if (ftruncate(fd, length) < 0) { + close_keep_errno(fd); + return linux_errno(); + } + close(fd); + return 0; + } + if (truncate(tx.host_path, length) < 0) return linux_errno(); return 0; @@ -2529,7 +2545,10 @@ int64_t sys_fchmodat(guest_t *g, return -LINUX_EBADF; int mac_flags = translate_at_flags(flags); - if (fchmodat(dir_ref.fd, tx.host_path, mode, mac_flags) < 0) { + if (tx.shm_redirect) + mac_flags |= AT_SYMLINK_NOFOLLOW; + if (fchmodat(path_tx_dirfd(&tx, &dir_ref), tx.host_path, mode, mac_flags) < + 0) { host_fd_ref_close(&dir_ref); return linux_errno(); } @@ -2679,11 +2698,14 @@ int64_t sys_fchownat(guest_t *g, return -LINUX_EBADF; int mac_flags = translate_at_flags(flags); + if (tx.shm_redirect) + mac_flags |= AT_SYMLINK_NOFOLLOW; + host_fd_t host_dirfd = path_tx_dirfd(&tx, &dir_ref); struct stat before_st; bool before_ok = - fstatat(dir_ref.fd, tx.host_path, &before_st, mac_flags) == 0; + fstatat(host_dirfd, tx.host_path, &before_st, mac_flags) == 0; - int host_rc = fchownat(dir_ref.fd, tx.host_path, owner, group, mac_flags); + int host_rc = fchownat(host_dirfd, tx.host_path, owner, group, mac_flags); int saved_errno = errno; struct stat after_st; @@ -2818,8 +2840,10 @@ int64_t sys_utimensat(guest_t *g, return linux_errno(); } } else { - if (utimensat(dir_ref.fd, path_arg, times_gva ? ts : NULL, mac_flags) < - 0) { + if (tx.shm_redirect) + mac_flags |= AT_SYMLINK_NOFOLLOW; + if (utimensat(path_tx_dirfd(&tx, &dir_ref), path_arg, + times_gva ? ts : NULL, mac_flags) < 0) { host_fd_ref_close(&dir_ref); return linux_errno(); } diff --git a/src/syscall/path.c b/src/syscall/path.c index 46a7053a..99ea32a0 100644 --- a/src/syscall/path.c +++ b/src/syscall/path.c @@ -17,6 +17,7 @@ #include "utils.h" +#include "runtime/procemu.h" #include "syscall/abi.h" #include "syscall/fuse.h" #include "syscall/path.h" @@ -177,6 +178,7 @@ int path_translate_at(guest_fd_t dirfd, tx->host_path = path; tx->proc_resolved = 0; tx->fuse_path = false; + tx->shm_redirect = false; if (!path) return 0; @@ -200,6 +202,26 @@ int path_translate_at(guest_fd_t dirfd, } } + /* /dev/shm/ maps into the per-UID host backing dir, through the + * same validated resolver as the open and stat intercepts. Only a + * non-empty flat leaf is redirected; bare "/dev/shm" and "/dev/shm/" + * stay on the sysroot path so the synthetic-directory intercepts keep + * answering for them. The resolver rejects "..", embedded '/', and + * empty names with EACCES. The early return skips sysroot resolution, + * the relative-containment recheck, and the sidecar lookup: the backing + * path is absolute, self-contained, and must never be sidecar-mapped. + * shm_redirect signals the redirect to callers, which must force nofollow + * on the host call; see dev_shm_resolve_path() for that invariant. + */ + if (!strncmp(tx->guest_path, "/dev/shm/", 9) && tx->guest_path[9] != '\0') { + if (proc_dev_shm_resolve(tx->guest_path + 9, tx->host_buf, + sizeof(tx->host_buf)) < 0) + return -1; + tx->host_path = tx->host_buf; + tx->shm_redirect = true; + return 0; + } + errno = 0; if ((flags & PATH_TR_CREATE) && sidecar_active() && sidecar_path_targets_reserved_name(tx->guest_path)) { diff --git a/src/syscall/path.h b/src/syscall/path.h index 6508b127..e21d940a 100644 --- a/src/syscall/path.h +++ b/src/syscall/path.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -27,11 +28,27 @@ typedef struct { const char *host_path; int proc_resolved; bool fuse_path; + /* Path was rewritten into the /dev/shm host backing dir. Follow-capable + * callers must force nofollow; see dev_shm_resolve_path() in procemu.c. + */ + bool shm_redirect; char proc_path[LINUX_PATH_MAX]; char guest_buf[LINUX_PATH_MAX]; char host_buf[LINUX_PATH_MAX]; } path_translation_t; +/* Select the host dirfd for a *at() call on a translated path. A /dev/shm + * redirect yields an absolute host path outside any guest dirfd's subtree; + * POSIX ignores dirfd for absolute paths, but selecting AT_FDCWD explicitly + * also avoids resolving against an unrelated descriptor and matches the + * historical unlinkat behavior. + */ +static inline host_fd_t path_tx_dirfd(const path_translation_t *tx, + const host_fd_ref_t *ref) +{ + return tx->shm_redirect ? AT_FDCWD : ref->fd; +} + /* Advance *pathp to the next '/'-separated component, skipping empty segments * from repeated slashes. Returns true with the component (not NUL-terminated) * reported through comp and len, leaving *pathp at its end; returns false once diff --git a/tests/manifest.txt b/tests/manifest.txt index 871557d3..d33d9527 100644 --- a/tests/manifest.txt +++ b/tests/manifest.txt @@ -75,6 +75,7 @@ test-ioctl-cloexec test-pty test-ioctl-fioasync test-getdents-refcount +test-dev-shm-paths [section] Threading tests test-thread # diff=skip diff --git a/tests/test-dev-shm-paths.c b/tests/test-dev-shm-paths.c new file mode 100644 index 00000000..98a346e0 --- /dev/null +++ b/tests/test-dev-shm-paths.c @@ -0,0 +1,276 @@ +/* + * /dev/shm path-syscall consistency tests + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Every path-taking syscall must resolve /dev/shm/ to the same per-UID + * host backing object that open() creates. The canonical failing sequence is + * LTP's library startup (lib/tst_test.c setup_ipc): open(O_CREAT|O_EXCL) on a + * /dev/shm path followed by chmod() of the same path; a split resolution + * turns the chmod into ENOENT and aborts the test before it starts. + * + * The second half guards the containment property that makes the shared + * redirect safe: a symlink planted inside the backing dir must never be + * followed to its host target by any shm-path operation. Legitimate shm + * objects are always regular files, so nofollow is transparent for correct + * programs. Note stat() on a shm symlink deliberately reports the link + * itself (lstat semantics) instead of following it as Linux would: on Linux + * the target would resolve inside the guest namespace, whereas here it would + * resolve on the host, so following is the escape. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test-harness.h" + +int passes = 0, fails = 0; + +static char shm_path[128]; +static char shm_path2[128]; +static char shm_link[128]; +static char shm_evil[128]; +static char victim_path[128]; + +static void name_fixtures(void) +{ + int pid = (int) getpid(); + snprintf(shm_path, sizeof(shm_path), "/dev/shm/elfuse_paths_%d", pid); + snprintf(shm_path2, sizeof(shm_path2), "/dev/shm/elfuse_paths2_%d", pid); + snprintf(shm_link, sizeof(shm_link), "/dev/shm/elfuse_link_%d", pid); + snprintf(shm_evil, sizeof(shm_evil), "/dev/shm/elfuse_evil_%d", pid); + snprintf(victim_path, sizeof(victim_path), "/tmp/elfuse-shm-victim-%d", + pid); +} + +static void cleanup_fixtures(void) +{ + unlink(shm_path); + unlink(shm_path2); + unlink(shm_link); + unlink(shm_evil); + unlink(victim_path); +} + +/* The exact LTP setup_ipc() sequence: create via open, adjust via chmod. */ +static int test_open_then_chmod(void) +{ + TEST("open O_CREAT then chmod same path"); + int fd = open(shm_path, O_CREAT | O_EXCL | O_RDWR, 0600); + if (fd < 0) { + FAIL("open O_CREAT"); + return -1; + } + if (chmod(shm_path, 0666) != 0) { + FAIL("chmod after open"); + close(fd); + return -1; + } + struct stat fd_st, path_st; + int ok = fstat(fd, &fd_st) == 0 && stat(shm_path, &path_st) == 0 && + fd_st.st_ino == path_st.st_ino && fd_st.st_dev == path_st.st_dev && + (path_st.st_mode & 0777) == 0666; + close(fd); + if (!ok) { + FAIL("path stat disagrees with the fd the open created"); + return -1; + } + PASS(); + return 0; +} + +static void test_metadata_ops_hit_same_object(void) +{ + TEST("chown/truncate/utimensat/access"); + if (chown(shm_path, (uid_t) -1, (gid_t) -1) != 0) { + FAIL("chown"); + return; + } + if (truncate(shm_path, 4096) != 0) { + FAIL("truncate"); + return; + } + struct stat st; + if (stat(shm_path, &st) != 0 || st.st_size != 4096) { + FAIL("truncate size not visible through path stat"); + return; + } + if (utimensat(AT_FDCWD, shm_path, NULL, 0) != 0) { + FAIL("utimensat"); + return; + } + if (access(shm_path, R_OK | W_OK) != 0) { + FAIL("access"); + return; + } + PASS(); +} + +static void test_statfs_resolves(void) +{ + TEST("statfs on a shm path"); + struct statfs sfs; + EXPECT_TRUE(statfs(shm_path, &sfs) == 0, "statfs"); +} + +static void test_rename_within_shm(void) +{ + TEST("rename within /dev/shm and back"); + if (rename(shm_path, shm_path2) != 0) { + FAIL("rename away"); + return; + } + struct stat st; + if (stat(shm_path, &st) == 0 || errno != ENOENT) { + FAIL("old name still visible"); + rename(shm_path2, shm_path); + return; + } + if (rename(shm_path2, shm_path) != 0) { + FAIL("rename back"); + return; + } + EXPECT_TRUE(stat(shm_path, &st) == 0, "object lost across rename"); +} + +static void test_link_and_unlink(void) +{ + TEST("link then unlink second name"); + if (link(shm_path, shm_link) != 0) { + FAIL("link"); + return; + } + struct stat a, b; + int ok = stat(shm_path, &a) == 0 && stat(shm_link, &b) == 0 && + a.st_ino == b.st_ino; + if (unlink(shm_link) != 0) { + FAIL("unlink second name"); + return; + } + EXPECT_TRUE(ok, "hard link is a different object"); +} + +static void test_unlink_removes(void) +{ + TEST("unlink then stat is ENOENT"); + if (unlink(shm_path) != 0) { + FAIL("unlink"); + return; + } + struct stat st; + EXPECT_TRUE(stat(shm_path, &st) == -1 && errno == ENOENT, + "object survived unlink"); +} + +static void test_symlink_leaf_is_not_followed(void) +{ + TEST("symlink leaf never reaches its target"); + int fd = open(victim_path, O_CREAT | O_EXCL | O_WRONLY, 0644); + if (fd < 0) { + FAIL("create victim"); + return; + } + if (write(fd, "victim", 6) != 6) { + FAIL("write victim"); + close(fd); + return; + } + close(fd); + + if (symlink(victim_path, shm_evil) != 0) { + FAIL("symlink into /dev/shm"); + return; + } + + /* Attack attempts through the shm path. Each must either fail or apply + * to the link itself; none may alter the victim. + */ + (void) chmod(shm_evil, 0777); + (void) truncate(shm_evil, 0); + struct timeval past[2] = {{1, 0}, {1, 0}}; + (void) utimes(shm_evil, past); + + struct stat vic; + if (stat(victim_path, &vic) != 0) { + FAIL("victim disappeared"); + return; + } + if ((vic.st_mode & 0777) != 0644 || vic.st_size != 6) { + FAIL("victim was modified through the shm symlink"); + return; + } + + struct stat link_st; + if (stat(shm_evil, &link_st) != 0 || !S_ISLNK(link_st.st_mode)) { + FAIL("shm stat followed the symlink"); + return; + } + + char target[256]; + ssize_t n = readlink(shm_evil, target, sizeof(target) - 1); + if (n < 0) { + FAIL("readlink"); + return; + } + target[n] = '\0'; + if (strcmp(target, victim_path) != 0) { + FAIL("readlink target mismatch"); + return; + } + + errno = 0; + if (open(shm_evil, O_RDWR) >= 0 || errno != ELOOP) { + FAIL("open followed the shm symlink"); + return; + } + if (unlink(shm_evil) != 0) { + FAIL("unlink symlink"); + return; + } + PASS(); +} + +static void test_flat_namespace_gate(void) +{ + TEST("nested and traversing names rejected"); + errno = 0; + if (chmod("/dev/shm/a/b", 0644) != -1 || errno != EACCES) { + FAIL("nested name not rejected with EACCES"); + return; + } + errno = 0; + if (chmod("/dev/shm/..", 0644) != -1 || errno != EACCES) { + FAIL("traversing name not rejected with EACCES"); + return; + } + PASS(); +} + +int main(void) +{ + printf("test-dev-shm-paths: /dev/shm path-syscall consistency\n"); + + name_fixtures(); + + if (test_open_then_chmod() == 0) { + test_metadata_ops_hit_same_object(); + test_statfs_resolves(); + test_rename_within_shm(); + test_link_and_unlink(); + test_unlink_removes(); + } + test_symlink_leaf_is_not_followed(); + test_flat_namespace_gate(); + + cleanup_fixtures(); + + SUMMARY("test-dev-shm-paths"); + return fails == 0 ? 0 : 1; +}