Skip to content
Merged
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
10 changes: 10 additions & 0 deletions src/core/bootstrap.c
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ static bool load_interpreter(guest_t *g,
return false;
}
interp_host_temp = true;
} else if (tx.is_dev_shm) {
/* A loader in /dev/shm is never legitimate, and elf_load() below
* opens it by path with no nofollow. Reject outright rather than
* risk following a symlink leaf onto the host. (The guest is not
* yet running, so no link can have been planted; a plain reject is
* race-free, unlike the runtime execve paths.)
*/
log_error("refusing /dev/shm interpreter: %s",
boot->elf_info.interp_path);
return false;
} else {
str_copy_trunc(boot->interp_resolved, tx.host_path,
sizeof(boot->interp_resolved));
Expand Down
45 changes: 36 additions & 9 deletions src/runtime/procemu.c
Original file line number Diff line number Diff line change
Expand Up @@ -683,11 +683,35 @@ 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.
/* Map a guest /dev/shm/<name> path to its host backing path, and gate the name.
*
* macOS has no /dev/shm, so elfuse backs POSIX shared memory with a per-UID
* host directory (/tmp/elfuse-shm-<uid>/). This is the single source of truth
* for that mapping. Callers proc_intercept_open, proc_intercept_stat, and
* path_translate_at (which records the hit in path_translation_t.is_dev_shm)
* all resolve through here, so one guest shm path never resolves two ways
* (e.g. open landing in the backing dir while chmod falls through to the
* sysroot).
*
* A POSIX shm name is always 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. This enforces the same shape, also rejects the
* ".." component (whole-component compare, so a flat name like "a..b" is fine),
* and returns EACCES (ENAMETOOLONG on overflow).
*
* The never-follow invariant lives here. On Linux /dev/shm is an in-namespace
* tmpfs, so a symlink planted at a shm leaf resolves inside that namespace.
* elfuse's backing store is a plain host directory, so the same symlink would
* resolve onto the host filesystem, escaping the sandbox. A symlink leaf is
* never legitimate anyway: glibc's shm_open (sysdeps/posix/shm_open.c) opens
* objects with O_NOFOLLOW. So every shm op must act on the leaf without
* following it. Because this resolver hands back an absolute host path that
* bypasses the sysroot, that duty is spread across syscall families and
* funneled through: path_translation_at_flags() (AT_SYMLINK_NOFOLLOW on the *at
* metadata calls), shm_open_leaf() (O_NOFOLLOW fd for truncate/chdir), proc
* open (O_NOFOLLOW), xattr (XATTR_NOFOLLOW), stat (lstat), linkat (clears
* AT_SYMLINK_FOLLOW), and statfs (answered synthetically, never touching the
* leaf).
*/
static int dev_shm_resolve_path(const char *guest_suffix,
char *host_path,
Expand All @@ -696,8 +720,8 @@ static int dev_shm_resolve_path(const char *guest_suffix,
const char *shm = shm_dir_path();
if (!shm)
return -1;
if (strstr(guest_suffix, "..") || strchr(guest_suffix, '/') ||
guest_suffix[0] == '\0') {
if (guest_suffix[0] == '\0' || strchr(guest_suffix, '/') ||
!strcmp(guest_suffix, "..")) {
errno = EACCES;
return -1;
}
Expand Down Expand Up @@ -3497,12 +3521,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
43 changes: 35 additions & 8 deletions src/syscall/exec.c
Original file line number Diff line number Diff line change
Expand Up @@ -160,18 +160,33 @@ static void exec_cleanup_inputs(char **argv,
free(envp_buf);
}

/* Open an execve image (binary or interpreter). For a shm redirect, force
* O_NOFOLLOW so a symlink leaf cannot point the exec at a host file; a real
* binary in /dev/shm still opens. See dev_shm_resolve_path().
*/
static int exec_open_image(const char *host_path, bool shm_nofollow)
{
int oflags = O_RDONLY | O_CLOEXEC;
if (shm_nofollow)
oflags |= O_NOFOLLOW;
return open(host_path, oflags);
}

static int exec_resolve_guest_host_path(const char *guest_path,
char *host_path,
size_t host_path_sz,
bool *host_path_temp)
bool *host_path_temp,
bool *shm_nofollow)
{
path_translation_t tx;
if (!guest_path || !host_path || host_path_sz == 0 || !host_path_temp) {
if (!guest_path || !host_path || host_path_sz == 0 || !host_path_temp ||
!shm_nofollow) {
errno = EINVAL;
return -1;
}

*host_path_temp = false;
*shm_nofollow = false;
if (path_translate_at(LINUX_AT_FDCWD, guest_path, PATH_TR_NONE, &tx) < 0)
return -1;
if (tx.fuse_path) {
Expand All @@ -190,18 +205,21 @@ static int exec_resolve_guest_host_path(const char *guest_path,
errno = ENAMETOOLONG;
return -1;
}
*shm_nofollow = tx.is_dev_shm;
return 0;
}

static int exec_resolve_interp_host_path(const char *sysroot,
const char *interp_guest_path,
char *interp_host_path,
size_t interp_host_path_sz,
bool *interp_host_temp)
bool *interp_host_temp,
bool *shm_nofollow)
{
char interp_candidate[LINUX_PATH_MAX];
elf_resolve_interp(sysroot, interp_guest_path, interp_candidate,
sizeof(interp_candidate));
*shm_nofollow = false;
if (strcmp(interp_candidate, interp_guest_path) != 0) {
size_t len = str_copy_trunc(interp_host_path, interp_candidate,
interp_host_path_sz);
Expand All @@ -214,7 +232,8 @@ static int exec_resolve_interp_host_path(const char *sysroot,
}

return exec_resolve_guest_host_path(interp_guest_path, interp_host_path,
interp_host_path_sz, interp_host_temp);
interp_host_path_sz, interp_host_temp,
shm_nofollow);
}

/* Read a NULL-terminated pointer array from guest memory. Each pointer in the
Expand Down Expand Up @@ -387,6 +406,10 @@ int64_t sys_execve(hv_vcpu_t vcpu,
char path_host_buf[LINUX_PATH_MAX];
const char *path_host = path;
bool path_host_temp = false;
/* Whether path_host is a shm redirect leaf (drives O_NOFOLLOW on the exec
* open). Re-evaluated whenever path_host is repointed to an interpreter.
*/
bool path_host_shm = false;
char interp_host_buf[LINUX_PATH_MAX];
bool interp_host_temp = false;

Expand Down Expand Up @@ -455,6 +478,7 @@ int64_t sys_execve(hv_vcpu_t vcpu,
path_host_temp = true;
} else {
str_copy_trunc(path_host_buf, tx.host_path, sizeof(path_host_buf));
path_host_shm = tx.is_dev_shm;
}
path_host = path_host_buf;
}
Expand All @@ -471,7 +495,7 @@ int64_t sys_execve(hv_vcpu_t vcpu,

/* Open the directly-executed file first and bind it to an fd to avoid
* TOCTOU. */
exec_fd = open(path_host, O_RDONLY | O_CLOEXEC);
exec_fd = exec_open_image(path_host, path_host_shm);
if (exec_fd < 0) {
err = linux_errno();
goto fail;
Expand Down Expand Up @@ -616,15 +640,17 @@ int64_t sys_execve(hv_vcpu_t vcpu,
goto fail;
interp_host_temp = true;
path_host = interp_host_buf;
path_host_shm = false;
} else {
str_copy_trunc(path_host_buf, interp_tx.host_path,
sizeof(path_host_buf));
path_host = path_host_buf;
path_host_shm = interp_tx.is_dev_shm;
}

/* Close old fd and open the new interpreter */
close(exec_fd);
exec_fd = open(path_host, O_RDONLY | O_CLOEXEC);
exec_fd = exec_open_image(path_host, path_host_shm);
if (exec_fd < 0) {
err = linux_errno();
goto fail;
Expand Down Expand Up @@ -734,14 +760,15 @@ int64_t sys_execve(hv_vcpu_t vcpu,
* itself via fd 3, so the aarch64-only interpreter pre-load below is
* skipped for rosetta exec.
*/
bool interp_shm = false;
if (!target_is_rosetta && elf_info.interp_path[0] != '\0') {
char sysroot_snap[LINUX_PATH_MAX];
bool have_sr =
proc_sysroot_snapshot(sysroot_snap, sizeof(sysroot_snap));
if (exec_resolve_interp_host_path(have_sr ? sysroot_snap : NULL,
elf_info.interp_path, interp_resolved,
sizeof(interp_resolved),
&interp_host_temp) < 0) {
&interp_host_temp, &interp_shm) < 0) {
log_error("execve: failed to resolve interpreter: %s",
elf_info.interp_path);
err = -LINUX_ENOEXEC;
Expand All @@ -754,7 +781,7 @@ int64_t sys_execve(hv_vcpu_t vcpu,

log_debug("execve: pre-validating interpreter: %s", interp_resolved);

interp_fd = open(interp_resolved, O_RDONLY | O_CLOEXEC);
interp_fd = exec_open_image(interp_resolved, interp_shm);
if (interp_fd < 0) {
log_error("execve: failed to open interpreter: %s",
interp_resolved);
Expand Down
32 changes: 30 additions & 2 deletions src/syscall/fs-stat.c
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,10 @@ 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) {
int mac_flags =
path_translation_at_flags(&tx, translate_at_flags(flags));
if (fstatat(path_translation_dirfd(&tx, &dir_ref), tx.host_path,
mac_st, mac_flags) < 0) {
rc = linux_errno();
goto done;
}
Expand Down Expand Up @@ -429,6 +431,32 @@ static int64_t sys_statfs_impl(guest_t *g,
}
}

/* Report /dev/shm and its leaves as tmpfs, from the backing dir. statfs()
* on the leaf would follow a symlink onto the host and leak the host fs
* identity, so answer synthetically; lstat is the nofollow existence probe.
*/
bool shm_root = !strcmp(tx.intercept_path, "/dev/shm") ||
!strcmp(tx.intercept_path, "/dev/shm/");
if (tx.is_dev_shm || shm_root) {
const char *shm_dir = proc_get_shm_dir();
if (!shm_dir)
return linux_errno();
if (tx.is_dev_shm) {
struct stat leaf_st;
if (lstat(tx.host_path, &leaf_st) < 0)
return linux_errno();
}
struct statfs shm_fs;
if (statfs(shm_dir, &shm_fs) < 0)
return linux_errno();
linux_statfs_t lin_st;
translate_statfs(&shm_fs, &lin_st); /* sets f_namelen = 255 */
lin_st.f_type = 0x01021994; /* TMPFS_MAGIC */
if (guest_write_small(g, buf_gva, &lin_st, sizeof(lin_st)) < 0)
return -LINUX_EFAULT;
return 0;
}

struct statfs mac_st;
if (statfs(tx.host_path, &mac_st) < 0)
return linux_errno();
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.is_dev_shm) ? 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.is_dev_shm) ? 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.is_dev_shm) ? 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.is_dev_shm) ? XATTR_NOFOLLOW : 0;
int ret = removexattr(tx.host_path, name, opts);
return ret < 0 ? linux_errno() : 0;
}
Expand Down
Loading
Loading