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; +}