Skip to content

Add --env and --clear-env launch flag support. - #295

Closed
henrybear327 wants to merge 4 commits into
sysprog21:mainfrom
henrybear327:oci/add_env_clear_env_launch_flags
Closed

Add --env and --clear-env launch flag support.#295
henrybear327 wants to merge 4 commits into
sysprog21:mainfrom
henrybear327:oci/add_env_clear_env_launch_flags

Conversation

@henrybear327

@henrybear327 henrybear327 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

In preparation for the OCI support, we need to support the ENV flag.


Summary by cubic

Adds --env/--clear-env, --user, and --workdir launch flags so launchers can set the guest environment, identity, and cwd precisely. Previously the guest always inherited the host environ, ran with compiled-in uid/gid (or fakeroot), and started in the host cwd; now flags control each and reject contradictory or unsafe requests before bring-up.

  • Implements guest_env_build to merge the environment like docker run -e: KEY=VALUE replaces or appends; bare KEY imports the host value; --clear-env starts empty; empty names are refused. elfuse_launch now accepts envp (NULL means inherit host environ).
  • Adds identity staging for --user: proc_set_initial_ids applies before proc_identity_init so auxv AT_UID/AT_GID match getuid/getgid. --fakeroot with a non-root or mixed --user is refused; --fakeroot --user 0:0 is allowed. Fail paths call proc_clear_initial_ids.
  • Adds --workdir (guest-absolute). Resolves under --sysroot, requires containment within the sysroot, and rejects /dev/shm and relative paths. Applies after sysroot casefold probing. On refusal, no VM is started.
  • Extends launch_args_t with envp, has_creds/uid/gid, and cwd_guest. Ownership rules updated: elf_path unlink now always belongs to elfuse_launch, even on pre-prepare refusals. Introduces strv_free for argv/env cleanup.

Tests & docs

  • Documents --user, --workdir, --env, and --clear-env in docs/usage.md, including rules and refusals.
  • Adds tests/test-guest-env-host.c (cross-product oracle for guest_env_build) and tests/test-launch-flags.sh (CLI parsing, pre-bring-up refusals, sysroot containment, and guest//proc environment sinks). Adds tests/test-env-dump.c.

Written for commit 28a6d5a. Summary will update on new commits.

Review in cubic

An OCI image front end needs to set the guest identity and working
directory without patching the runtime; both flags map onto
launch_args_t fields and `elfuse-oci run` drives exactly this
interface.

The --user identity is staged before bring-up (proc_set_initial_ids)
so the auxv AT_UID/AT_GID snapshot taken by build_linux_stack matches
what getuid()/getgid() later report. proc_identity_init consumes the
staged value, and the elfuse_launch fail path calls
proc_clear_initial_ids: a bring-up that fails before proc_init would
otherwise leave the value staged for the next launch in the same host
process.

--workdir rejects non-absolute paths up front instead of silently
resolving them against the host cwd, and is applied by elfuse_launch
after the casefold probe so the translation sees the sysroot's real
case behavior. The resolved host path must sit inside the sysroot
prefix: proc_resolve_sysroot_path falls back to the host spelling
when the sysroot has no entry at the path, which is the overlay
contract for guest syscalls but would start the guest in a
same-named host directory here, so the launch refuses it.

--fakeroot and a non-root --user are refused together. Fakeroot starts
the guest as uid/gid 0 and uid_is_permitted() grants every id switch
on that basis; a non-root --user would keep that grant while the guest
reported an unprivileged uid, letting the guest call setuid(0) at
will. The refusal lives in elfuse_launch beside the Rosetta GDB check,
so every launcher inherits the privilege rule, and it exits through
the shared fail unwind so a FUSE-materialized temp ELF is unlinked
even when bring-up never starts.

tests/test-launch-flags.sh covers the refusal, the parse rules, and
the --workdir sysroot containment; tests/test-identity-override-host.c
pins the staging consume-once and clear semantics as regression guards
(the cross-launch leak needs two bring-ups in one host process, which
no launcher performs). docs/usage.md documents both flags, the
containment rule, and the two-sided root requirement behind the
fakeroot refusal.
path_translate_at redirects /dev/shm/<leaf> into the per-UID host
backing dir and returns before sysroot resolution, so elfuse_launch ran
only sys_chdir's real-directory branch for such a workdir. The plain
chdir followed a symlink leaf that shm_open_leaf's O_NOFOLLOW fd
refuses, and proc_cwd_refresh published the backing location, so getcwd
reported /private/tmp/elfuse-shm-<uid>/<leaf> where a guest chdir into
the same directory reports /dev/shm/<leaf>.

Entering the leaf correctly needs that O_NOFOLLOW fd and the virtual-cwd
publish, which would make launch.c a second holder of the never-follow
duty dev_shm_resolve_path enumerates, for a workdir no image asks for.
The flag refuses the path instead, on the same grounds
guest_bootstrap_prepare refuses a /dev/shm ELF interpreter.

The test creates the leaf first, so the case measures the refusal rather
than a missing directory, and chmods the backing root because
create_private_dir rejects a group or other permission bit.

The test-launch-flags.sh header claimed every refusal happens before any
VM exists. guest_bootstrap_prepare creates and initializes the VM before
the workdir block runs, so the header now names both timings: --user
before the VM is created, --workdir during bring-up.
An OCI image config carries an environment the guest must start with, so
elfuse needs a merge that follows `docker run -e` rather than one that
approximates it. guest_env_build in src/core/guest-env.c is that merge,
kept out of main() so it can be exercised against base vectors no live
process could be given: the host environment arrives as a parameter, so
nothing in the module reads global state.

"KEY=VALUE" replaces that name where env_find() locates it and appends
otherwise; a bare "KEY" imports the launcher's value, appending nothing
rather than an empty value when the launcher does not set it; clear_env
drops the base while leaving a bare "KEY" resolvable. An empty variable
name is refused, as setenv(3) refuses it.

    host_env         overrides   *out_envp
    --------------   ---------   ----------
    [0] PATH=/bin    PATH=/opt   PATH=/opt    replaced in place
    [1] WEIRD                                 dropped: no '='
    [2] TERM=xterm               TERM=xterm   forwarded
    [3] PATH=/dup                             dropped: dup of [0]
                     NEW=1       NEW=1        appended

An entry the overrides cannot address is dropped rather than forwarded:
entry_key_len() returns 0 for [1], and env_find() matches only [0] for
[3]. Forwarding either would leave the guest holding both an override
and the entry it was meant to replace. getenv(3) reads the first copy
either way, but anything walking environ directly, /proc/self/environ
included, would see the stale one.

free_guest_argv generalizes to strv_free in src/utils.h, which frees the
guest argv and a built envp alike.

tests/test-guest-env-host.c enumerates the merge as a cross product of
base x clear_env x override sequence, holding every cell to a reference
merge and to structural invariants no cell may violate. Its alphabet
carries a refused token, so a rejection lands at every base and either
override position rather than at one hand-picked base, and one base sets
a name to the empty string, so a bare "KEY" import separates a name the
launcher set to "" from one it never set. Both widen a coverage guard
rather than fix an observed failure. The oracle compares whole names
with strcmp where env_find compares klen bytes and then the boundary
byte, which is the axis it is independent on; it shares the drop and
refusal rules, so it rules out a mechanism bug rather than a wrong
reading of `docker run -e`.
`elfuse-oci run` launches every guest as `elfuse --clear-env --env K=V
... --`, so a front end needs to set the guest environment through the
CLI rather than by patching the runtime. Both flags map onto
launch_args_t beside --user and --workdir.

The new launch_args_t envp field generalizes elfuse_launch's hard-coded
environ on guest_env_build's rule: a NULL vector means the host environ,
which is what leaves callers that set neither flag unchanged. main()
collects the --env tokens as borrowed argv pointers into one argc-sized
allocation and builds the vector before runtime_set_process_title
clobbers the argv block, which is what lets the refusal name the
offending token and keeps a malformed --env from reaching
--create-sysroot.

tests/test-launch-flags.sh adds only what the guest_env_build cross
product cannot reach: main() collecting the tokens out of argv,
build_linux_stack copying the vector onto the initial guest stack, and
the /proc/self/environ sink, which is held to the same entries
build_linux_stack copied rather than compared against another sink of
one run. tests/test-env-dump.c is the guest that prints its environ for
those lanes. docs/usage.md documents the import and replace rules and
the empty-name refusal.
@jserv

jserv commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

This branch is currently stacked on top of #286

Never submit stacking pull requests!

@jserv jserv closed this Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants