Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions src/runtime/procemu.c
Original file line number Diff line number Diff line change
Expand Up @@ -683,11 +683,27 @@ static int proc_parse_fd_index(const char *path,
return (int) n;
}

/* Resolve a /dev/shm/<suffix> 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/<suffix> 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).

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.

*
* 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,
Expand Down Expand Up @@ -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/<name> files: check the host temp dir */
/* /dev/shm/<name> 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
Expand Down
3 changes: 2 additions & 1 deletion src/syscall/fs-stat.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
8 changes: 4 additions & 4 deletions src/syscall/fs-xattr.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
106 changes: 65 additions & 41 deletions src/syscall/fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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/<name> 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/<name> 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();
}
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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
Expand All @@ -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());
Expand All @@ -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));
Expand All @@ -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());
}
Expand Down Expand Up @@ -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();
}
Expand All @@ -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();
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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
Expand All @@ -2283,18 +2283,18 @@ 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);
return -LINUX_EPERM;
}
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();
Expand Down Expand Up @@ -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) <

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.

0) {
Comment on lines +2377 to +2378

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

host_fd_ref_close(&dir_ref);
return linux_errno();
}
Expand Down Expand Up @@ -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);

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.

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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
22 changes: 22 additions & 0 deletions src/syscall/path.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#include "utils.h"

#include "runtime/procemu.h"
#include "syscall/abi.h"
#include "syscall/fuse.h"
#include "syscall/path.h"
Expand Down Expand Up @@ -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;
Expand All @@ -200,6 +202,26 @@ int path_translate_at(guest_fd_t dirfd,
}
}

/* /dev/shm/<leaf> 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;

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.

return 0;
}

errno = 0;
if ((flags & PATH_TR_CREATE) && sidecar_active() &&
sidecar_path_targets_reserved_name(tx->guest_path)) {
Expand Down
Loading
Loading