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
36 changes: 36 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Supported user-facing options:
| `--fakeroot` | Start the guest as uid/gid 0 with full emulated capabilities (also `ELFUSE_FAKEROOT=1`) |
| `--gdb PORT` | Listen for a GDB RSP client on `PORT` (aarch64 guests only) |
| `--gdb-stop-on-entry` | Stop before the first guest instruction |
| `--user UID[:GID]` | Run the guest as `UID`, and `GID` when given (defaults to `UID`). Numeric only |
| `--workdir DIR` | Guest-absolute initial working directory, resolved under `--sysroot` |
| `--` | End `elfuse` option parsing; remaining tokens are guest argv |

`ELFUSE_FAKEROOT_EXEC` has no flag form. It names one executable, by absolute
Expand All @@ -46,6 +48,40 @@ only bounds a single `hv_vcpu_run()` iteration before the host regains control,
which is what allows host-side timers and signals to be observed promptly.
Setting `--timeout 0` disables this watchdog for long-running CPU-bound guests.

## Guest Identity And Working Directory

`--user` and `--workdir` select what the guest starts as and where it starts.
A contradictory `--user` request is rejected before the VM is created, and
`--workdir` is resolved during bring-up, before the first guest instruction,
so a bad request fails with a diagnostic instead of launching a guest that
runs as something other than what was asked for.

`--user UID[:GID]` sets the identity the guest reports through `getuid` and
`getgid`. It does not change the host process credentials: elfuse translates the
guest's syscalls, so the number the guest sees is elfuse's to choose. The spec is
numeric, and a bare `UID` sets the group to the same value. Symbolic names are
resolved against the image `/etc/passwd` and `/etc/group` one layer up, by
`elfuse-oci`.

`--fakeroot` cannot be combined with a non-root `--user`. Fakeroot starts the guest
as uid/gid 0, and the setuid permission check grants every id switch on that basis,
so a guest that reported an unprivileged uid could still call `setuid(0)` at will.
Both halves must be root, which makes `--fakeroot --user 0:0` valid and
`--fakeroot --user 0:1000` a refusal.

`--workdir DIR` takes a guest-absolute path and is rejected otherwise. A relative
path would be resolved against the host working directory, silently starting the
guest outside the intended tree. The path is translated through `--sysroot` and
then entered, the same way a guest `chdir` into a real directory is handled,
with one launch-time restriction: the resolved directory must sit inside the
sysroot. For a path the sysroot does not hold, a guest syscall falls back to
the host, but a workdir that exists only on the host would start the guest
outside the requested tree, so the launch refuses it. FUSE-mounted,
`/proc`-virtual, and `/dev/shm` directories are not supported through this
flag: a guest `chdir` into `/dev/shm` does two things this flag does not (it
refuses a symlink leaf, and it keeps `getcwd` reporting the `/dev/shm`
spelling rather than the backing location).

## Common Launch Patterns

Run a statically linked guest binary:
Expand Down
9 changes: 8 additions & 1 deletion mk/tests.mk
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ ELFUSE_HOST_NOFILE_MIN ?= $(shell bash "$(CURDIR)/tests/test-config.sh" --host-n
test-mremap-tail-emfile \
test-proctitle-host test-proctitle-low-stack \
test-sysroot-procfs-exec test-sysroot-fd-magiclink \
test-timeout-disable test-fuse-alpine \
test-timeout-disable test-launch-flags \
test-fuse-alpine \
test-sysroot-nofollow test-sysroot-chdir test-sysroot-symlink-escape \
test-sysroot-dotdot test-sysroot-openat2-walk \
test-sysroot-inotify-names test-sysroot-exec-names \
Expand Down Expand Up @@ -235,6 +236,7 @@ check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage test-config \
$(call run-lane,test-case-collision-fallback,case collisions on a folding sysroot)
$(call run-lane,test-fuse-alpine,Alpine sysroot FUSE validation)
$(call run-lane,test-timeout-disable,timeout=0 validation)
$(call run-lane,test-launch-flags,launch flag rejection)
$(call run-lane,test-rosetta-cli,rosetta CLI gating)
$(call run-lane,test-bench-guardrail,hot-syscall guardrail)

Expand Down Expand Up @@ -1024,6 +1026,11 @@ test-sysroot-fd-magiclink: $(ELFUSE_BIN) $(BUILD_DIR)/test-fd-magiclink
test-timeout-disable: $(ELFUSE_BIN) $(TEST_HELLO_DEP)
@$(ELFUSE_BIN) --timeout 0 $(TEST_DIR)/test-hello > /dev/null

## Verify --user / --workdir / --fakeroot reject contradictory requests before
## guest bring-up.
test-launch-flags: $(ELFUSE_BIN) $(TEST_HELLO_DEP)
@bash tests/test-launch-flags.sh $(ELFUSE_BIN) $(TEST_DIR)/test-hello

## Check the --help and argument-error usage synopses against each other
test-usage-synopsis: $(ELFUSE_BIN)
@bash tests/test-usage-synopsis.sh $(ELFUSE_BIN)
Expand Down
75 changes: 75 additions & 0 deletions src/core/launch.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@

#include <Hypervisor/Hypervisor.h>
#include <Hypervisor/hv_vcpu.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "core/bootstrap.h"
Expand All @@ -29,6 +31,7 @@
#include "runtime/futex.h" /* futex_interrupt_request */
#include "runtime/procemu.h" /* proc_pty_release_process_slaves */
#include "runtime/thread.h"
#include "syscall/path.h"
#include "syscall/proc.h"
#include "syscall/wakeup-pipe.h"

Expand Down Expand Up @@ -69,6 +72,25 @@ int elfuse_launch(const launch_args_t *args)
? args->guest_argv[0]
: args->elf_path;

/* uid_is_permitted() grants every setuid under fakeroot because fakeroot
* starts the guest as uid/gid 0. A non-root --user keeps that grant while
* reporting an unprivileged uid, so the guest could call setuid(0).
*/
if (proc_fakeroot_enabled() && args->has_creds &&
(args->uid != 0 || args->gid != 0)) {
log_error(
"--fakeroot runs the guest as uid/gid 0 and cannot be combined "
"with --user %u:%u",
args->uid, args->gid);
goto fail;
}

/* Stage --user before bring-up; proc.h states why it cannot be applied
* afterwards.
*/
if (args->has_creds)
proc_set_initial_ids(args->uid, args->gid);

if (guest_bootstrap_prepare(
&g, args->elf_path, elf_host_temp, elf_guest_path, args->sysroot,
args->guest_argc, args->guest_argv, envp_use, shim_bin,
Expand Down Expand Up @@ -108,6 +130,57 @@ int elfuse_launch(const launch_args_t *args)
proc_set_sysroot_casefold(false);
}

/* Placed after the casefold probe so path_translate_at() sees the
* sysroot's real case behavior.
*/
if (args->cwd_guest && args->cwd_guest[0] != '\0') {
Comment thread
henrybear327 marked this conversation as resolved.
path_translation_t tx;
if (path_translate_at(LINUX_AT_FDCWD, args->cwd_guest, PATH_TR_NONE,
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
&tx) < 0) {
log_error("failed to resolve working directory %s: %s",
args->cwd_guest, strerror(errno));
goto fail;
}
/* A shm leaf needs sys_chdir's O_NOFOLLOW fd and virtual-cwd publish;
* entering it here would add a holder of the never-follow invariant
* dev_shm_resolve_path() enumerates. Refuse instead.
*/
if (tx.is_dev_shm) {
log_error("--workdir %s: /dev/shm is not supported",
args->cwd_guest);
goto fail;
}
/* proc_resolve_sysroot_path() falls back to the host spelling for a
* path the sysroot does not hold, which would start the guest in a
* same-named host directory outside the tree --workdir named.
*/
if (args->sysroot) {
char sr[LINUX_PATH_MAX];
if (!proc_sysroot_snapshot(sr, sizeof(sr))) {
log_error("failed to read the sysroot prefix for --workdir %s",
args->cwd_guest);
goto fail;
}
/* Same carve-out as path_dirent_dir_holds_escapes(): "--sysroot /"
* owns every host path, but path_prefix_match on a bare separator
* accepts "/" alone.
*/
size_t srlen = strlen(sr);
if (srlen > 1 && !path_prefix_match(tx.host_path, sr, srlen)) {
log_error("--workdir %s does not resolve inside the sysroot",
args->cwd_guest);
goto fail;
}
}
if (chdir(tx.host_path) < 0) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
log_error("failed to set working directory %s: %s", args->cwd_guest,
strerror(errno));
goto fail;
}
if (proc_cwd_refresh() < 0)
proc_cwd_invalidate();
Comment thread
henrybear327 marked this conversation as resolved.
}

hv_vcpu_t vcpu;
hv_vcpu_exit_t *vexit;
if (guest_bootstrap_create_vcpu(&g, &boot, args->verbose, &vcpu, &vexit) <
Expand Down Expand Up @@ -203,7 +276,9 @@ int elfuse_launch(const launch_args_t *args)
fail:
/* Bring-up failed: unwind whatever exists so far, including the temp
* unlink this side owns past the prepare call (contract in launch.h).
* Staged --user credentials are dropped too (proc.h).
*/
proc_clear_initial_ids();
if (guest_initialized)
guest_destroy(&g);
if (elf_host_temp)
Expand Down
24 changes: 18 additions & 6 deletions src/core/launch.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*
* elfuse_launch is the single entry point for "run a guest binary in a
* fresh HVF VM until it exits". main() is its only in-tree caller; keeping
* bring-up behind one struct is what lets another front end (the planned
* OCI run helper) reuse this path instead of growing a second bring-up.
* fresh HVF VM until it exits". main() is its only caller; keeping bring-up
* behind one struct is what lets a front end select the guest identity and
* cwd through the CLI instead of growing a second bring-up.
*
* The function owns the guest_t, the vCPU, the GDB stub, the run loop, the
* diagnostic dumps, and guest teardown; it does NOT own the elf_path /
Expand All @@ -33,9 +33,9 @@ typedef struct {

/* elf_path is a FUSE-materialized temp to unlink once
* guest_bootstrap_prepare has loaded it (kept for Rosetta guests, which
* reopen the path). The caller owns the unlink on any pre-prepare
* failure; elfuse_launch owns it from the prepare call onward,
* including a prepare that fails.
* reopen the path). Ownership of the unlink transfers to elfuse_launch
* at the call: every failure path inside it, refusals before the
* prepare call included, unlinks a temp elf_path.
*/
bool elf_host_temp;

Expand All @@ -52,6 +52,18 @@ typedef struct {
int guest_argc;
const char **guest_argv;

/* When true, uid/gid are staged before bring-up so the auxv AT_UID/AT_GID
* snapshot and getuid()/getgid() agree. When false the guest runs under
* GUEST_UID/GUEST_GID (0 under fakeroot), never the host identity.
*/
bool has_creds;
uint32_t uid, gid;

/* Guest-absolute initial working directory, resolved under sysroot.
* NULL inherits the host cwd.
*/
const char *cwd_guest;

/* GDB Remote Serial Protocol port (0 disables the stub) and whether
* to halt before the first guest instruction.
*/
Expand Down
77 changes: 75 additions & 2 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@ static int parse_int_arg(const char *s, int min, int max, int *out)
return 0;
}

/* Parse one --user id component, stopping at @end. strtoul would take a sign
* and negate the unsigned result, so "-0" parses as root; an id is digits.
*/
static int parse_id_component(const char *s, const char **end, uint32_t *out)
{
if (*s < '0' || *s > '9')
return -1;
unsigned long long value = 0;
for (; *s >= '0' && *s <= '9'; s++) {
value = value * 10 + (unsigned long long) (*s - '0');
if (value > UINT32_MAX)
return -1;
}
*end = s;
*out = (uint32_t) value;
return 0;
}

static int resolve_guest_elf_host_path(const char *elf_guest_path,
char *elf_host_path,
size_t elf_host_path_sz,
Expand Down Expand Up @@ -201,7 +219,8 @@ static int host_dc_zva_assert(void)
#define ELFUSE_USAGE_BODY(sep) \
"usage: elfuse [--verbose] [--timeout N] [--sysroot PATH]" sep \
"[--create-sysroot PATH] [--no-rosetta] [--fakeroot]" sep \
"[--gdb PORT] [--gdb-stop-on-entry] <elf-path> [args...]"
"[--gdb PORT] [--gdb-stop-on-entry]" sep \
"[--user UID[:GID]] [--workdir DIR] <elf-path> [args...]"

#define ELFUSE_USAGE ELFUSE_USAGE_BODY(" ")
#define ELFUSE_USAGE_WRAPPED ELFUSE_USAGE_BODY("\n ")
Expand Down Expand Up @@ -229,6 +248,9 @@ int main(int argc, char **argv)
int gdb_port = 0;
bool gdb_stop_on_entry = false;
bool fakeroot = false;
bool has_creds = false;
uint32_t uid = 0, gid = 0;
char *workdir = NULL;
int arg_start = 1;
/* Everything the shared cleanup label reads is declared and initialized
* here, above the option loop, so any later error path can `goto cleanup`:
Expand Down Expand Up @@ -296,6 +318,11 @@ int main(int argc, char **argv)
"Protocol on PORT\n"
" --gdb-stop-on-entry Halt before the first guest "
"instruction\n"
" --user UID[:GID] Run the guest as UID (and GID; "
"defaults to UID). Numeric; elfuse-oci resolves symbolic "
"names\n"
" --workdir DIR Guest-absolute initial working "
"directory (resolved under --sysroot)\n"
"\n"
"Environment:\n"
" ELFUSE_NO_ROSETTA=1 Same as --no-rosetta\n"
Expand Down Expand Up @@ -368,6 +395,47 @@ int main(int argc, char **argv)
} else if (!strcmp(argv[arg_start], "--gdb-stop-on-entry")) {
gdb_stop_on_entry = true;
arg_start++;
} else if (!strcmp(argv[arg_start], "--user") && arg_start + 1 < argc) {
const char *spec = argv[arg_start + 1], *end;
uint32_t u, g;
if (parse_id_component(spec, &end, &u) < 0) {
log_error("invalid --user UID: %s", spec);
goto cleanup;
}
g = u;
if (*end == ':') {
const char *gend;
if (parse_id_component(end + 1, &gend, &g) < 0 ||
*gend != '\0') {
log_error("invalid --user UID:GID: %s", spec);
goto cleanup;
}
} else if (*end != '\0') {
log_error("invalid --user spec: %s", spec);
goto cleanup;
}
uid = u;
gid = g;
has_creds = true;
arg_start += 2;
} else if (!strcmp(argv[arg_start], "--workdir") &&
arg_start + 1 < argc) {
/* A relative path resolves against the host cwd, silently
* starting the guest outside the intended tree. strdup because
* runtime_set_process_title() clobbers the argv block.
*/
if (argv[arg_start + 1][0] != '/') {
log_error("--workdir requires a guest-absolute path, got %s",
argv[arg_start + 1]);
goto cleanup;
}
free(workdir);
workdir = strdup(argv[arg_start + 1]);
if (!workdir) {
log_error("out of memory");
goto cleanup;
}
arg_start += 2;
} else if (!strcmp(argv[arg_start], "--")) {
arg_start++;
break;
Expand Down Expand Up @@ -616,14 +684,18 @@ int main(int argc, char **argv)
* retains ownership of the original argv (proctitle above), the sysroot
* mount (detached at the cleanup label after the guest exits so the
* mount stays live for the whole run), host cwd, and the heap elf_path /
* sysroot_path / guest_argv copies.
* sysroot_path / guest_argv / workdir copies.
*/
launch_args_t largs = {
.elf_path = elf_host_path,
.elf_host_temp = elf_host_temp,
.sysroot = sysroot,
.guest_argc = guest_argc,
.guest_argv = guest_argv,
.has_creds = has_creds,
.uid = uid,
.gid = gid,
.cwd_guest = workdir,
.gdb_port = gdb_port,
.gdb_stop_on_entry = gdb_stop_on_entry,
.timeout_sec = timeout_sec,
Expand Down Expand Up @@ -652,6 +724,7 @@ int main(int argc, char **argv)
free_guest_argv(guest_argv, guest_argc);
free(elf_path);
free(sysroot_path);
free(workdir);
if (elf_host_temp)
unlink(elf_host_path);

Expand Down
Loading
Loading