Repository navigation
feat(agentd): an image-set default user for execs that omit user (#595) - #600
Merged
Merged
Conversation
laithalsaadoon
added a commit
that referenced
this pull request
Oct 12, 2026
The CLI's `guards::parity::the_cli_answers_the_shared_case_corpus_against_its_fakes` spawns microvms-core's `parity-tunnel-peer` example from the test binary's target directory. A mutant's `cargo test -p microvms-cli` never builds another package's example, so a change with mutants in microvms-cli failed cargo-mutants' unmutated baseline (exit 4, "build the fixture with mise run parity:fixtures"), as shard 3 of 4 did on #600. `ci:mutants` now depends on `parity:fixtures`, which builds the example into the target the `--in-place` run tests from. Shard 3 of 4 run locally with this: baseline ok in 31 s build + 9 s test, 1 mutant, unviable.
3 tasks done
laithalsaadoon
added a commit
that referenced
this pull request
Oct 12, 2026
The CLI's `guards::parity::the_cli_answers_the_shared_case_corpus_against_its_fakes` spawns microvms-core's `parity-tunnel-peer` example from the test binary's target directory. A mutant's `cargo test -p microvms-cli` never builds another package's example, so a change with mutants in microvms-cli failed cargo-mutants' unmutated baseline (exit 4, "build the fixture with mise run parity:fixtures"), as shard 3 of 4 did on #600. `ci:mutants` now depends on `parity:fixtures`, which builds the example into the target the `--in-place` run tests from. Shard 3 of 4 run locally with this: baseline ok in 31 s build + 9 s test, 1 mutant, unviable.
) A start request that omits `user`, from `exec` or `run --exec`, runs as the daemon's uid, which is root, so every workload is guest root unless each caller remembers to demote it. The daemon now reads `AGENTD_DEFAULT_USER` from the image's environment at startup. A start that omits `user` resolves that string at each request exactly as it would resolve it in `user` (AGENTD-22), a request's own `user` still runs as given, `root` and `0` included (AGENTD-23), and a value the guest's /etc/passwd lacks refuses the start as `unknown_user` with a detail that mentions the variable (AGENTD-24). Unset, nothing changes. Workload hook handlers still run as root. `/v1/health` doesn't report the default: `Health` is constructible field by field, and a new field fails `semver:check` (`constructible_struct_adds_field`, measured), so the daemon logs it at startup instead. The agent recipe's `chown` of /workspace and a sync's in-guest deletions need root, so they ask for uid 0 by number instead of omitting `user`. The model, the Gherkin tier, the fuzz harness and the handler tests cover the three new requirements; the live layer is waived because the live images set no default.
The CLI's `guards::parity::the_cli_answers_the_shared_case_corpus_against_its_fakes` spawns microvms-core's `parity-tunnel-peer` example from the test binary's target directory. A mutant's `cargo test -p microvms-cli` never builds another package's example, so a change with mutants in microvms-cli failed cargo-mutants' unmutated baseline (exit 4, "build the fixture with mise run parity:fixtures"), as shard 3 of 4 did on #600. `ci:mutants` now depends on `parity:fixtures`, which builds the example into the target the `--in-place` run tests from. Shard 3 of 4 run locally with this: baseline ok in 31 s build + 9 s test, 1 mutant, unviable.
…oads (#595) A numeric AGENTD_DEFAULT_USER was resolved the way a request's own digits are: a bare uid in the daemon's group. AGENTD_DEFAULT_USER=1000 ran workloads as uid 1000 in gid 0, so they could read and write whatever root's group can. The default now resolves to a row of the guest's /etc/passwd, by name or by the uid its digits spell. The child gets that row's uid, primary gid, HOME, USER and LOGNAME. A default with no row, digits included, refuses each start without a `user` as 400 unknown_user. The detail names the variable and says the image needs a passwd row (AGENTD-24). A request's own digits still run as a bare uid in the daemon's group (AGENTD-16). The model gains numbered and unnumbered defaults, and the fuzz harness, the BDD tier and the handler tests cover both. Uploads stayed root's, so a nameless exec under the default user couldn't write the tree `run . --exec` and `cp` had just uploaded. With the variable set, what PUT /v1/fs/tar and PUT /v1/fs/file create (files, directories, symlinks, and created parents) now belongs to the default user's uid and primary gid. What already existed keeps its owner, and a replaced file or symlink keeps the owner of the entry it replaced. Each chown is fchownat(fd, "", AT_EMPTY_PATH) on the entry's own descriptor, never a path: tar members through the extraction's openat2 walk, and a symlink opened as itself and checked to be a symlink first. A default with no row refuses the upload with the same 400 unknown_user before the body is read. Unset, uploads stay root's. The fs API has no directory-creation route, so these two routes are the whole surface. The schema lists the new 400 on both. The changelog says an image that sets the variable needs a client at this release or later, since an older one sends the recipe's chown and a sync's deletions with no `user`. fs.rs's directory and link open flags become `.union()` consts that every open shares, so cargo-mutants has no equivalent `|` mutant to report. The harden-agentd and default-user FAIL_TO_PASS patches are rebased onto the new fs.rs, and new seeded faults in verify/guards/faults/default-user.toml cover each rule above.
#594 merged statements that an exec naming no user runs as root. With AGENTD_DEFAULT_USER set it runs as the image's user, so the trivy exception and TRUST.md's MMDS paragraph say when an exec runs as root: with user 0, or without user on an image that doesn't set the variable.
laithalsaadoon
force-pushed
the
feature/agentd-default-exec-user
branch
from
October 12, 2026 22:13
ccdb6a4 to
7388f52
Compare
d6a33c9 gave the default user what PUT /v1/fs/tar and PUT /v1/fs/file create. Review found that this hands the default user files outside the named tree. The owned walk follows a symlink that's already there with only RESOLVE_NO_MAGICLINKS. A workload running as the default user can swap a directory it owns from an earlier sync for a symlink to a directory root reads, and the next upload to that path extracts into the link's target and gives every new entry to the default user. The file route has the same hole twice: a planted symlink, and a race between the walk's descriptor and the reopen by path. This takes that half back out. fs.rs, schema.rs and agentd's Cargo.toml go back to main's versions, and so do the ten harden-agentd FAIL_TO_PASS patches rebased onto the owned fs.rs. The upload faults leave default-user.toml. Uploads don't read AGENTD_DEFAULT_USER again, so a default with no row no longer refuses one. The agent recipe's and the sync's comments go back to saying the daemon writes uploads as root. PROTOCOL.md's Upload ownership section, TRUST.md, the guest Dockerfile guide and the changelog now state the limit. Uploads stay root's: a tar's modes are masked to 0755 at most, and PUT /v1/fs/file creates its parents 0755 and its file 0644 unless the request sets a mode. So the default user can read an uploaded tree but can't write into it. An exec that must write there runs with user 0, or a root chown hands the tree over first. The guide no longer offers the variable as the default for every exec and `run --exec`. Safe upload ownership is its own issue. The numeric default's row and gid (Finding 1) and the changelog's client-version sentence (Finding 3) stay as they were. default_user_row is private again now that fs.rs doesn't call it. The two default-user FAIL_TO_PASS patches are regenerated against f410ec2.
35 of 45 tasks
bonk-ai Bot
added a commit
that referenced
this pull request
Oct 13, 2026
tech-debt.md loses items 5 (napi generator pin, fixed by #576) and 10 (lifecycle commentary, fixed by #579) and the bullets that cited them; item 8 now names #576's agreement test and the proof-runner copy it leaves out. debugging-guide.md drops the Serve-not-Future entry that #583's accept loop retired. 15 citations #600 and #601 moved on those pages are re-pointed by exact text match. The see-also footers on 19 pages drop their hand-written citation counts (AGENTS.md, Counts in docs), and .vale/warnings.json lowers 25 ceilings to this tree's counts. docs:check EXIT=0 (240 s), prose:check EXIT=0 (0 errors, 4069 warnings).
2 tasks done
bonk-ai Bot
added a commit
that referenced
this pull request
Oct 13, 2026
…607) tech-debt.md loses items 5 (napi generator pin, fixed by #576) and 10 (lifecycle commentary, fixed by #579) and the bullets that cited them; item 8 now names #576's agreement test and the proof-runner copy it leaves out. debugging-guide.md drops the Serve-not-Future entry that #583's accept loop retired. 15 citations #600 and #601 moved on those pages are re-pointed by exact text match. The see-also footers on 19 pages drop their hand-written citation counts (AGENTS.md, Counts in docs), and .vale/warnings.json lowers 25 ceilings to this tree's counts. docs:check EXIT=0 (240 s), prose:check EXIT=0 (0 errors, 4069 warnings). Co-authored-by: bonk-ai[bot] <269762587+bonk-ai[bot]@users.noreply.github.com>
bonk-ai Bot
added a commit
that referenced
this pull request
Oct 14, 2026
604 path:line citations on 15 hand-written pages pointed a few lines off after #600 and #601 moved their source lines. Each move is the doc line's blame-commit block found at HEAD by exact text and by git's own diff line mapping, or a read-only audit against the sentence for the 20 rows neither settled. state-machines.md's VmState section now says what ResumeRequested does after SuspendedVmEnded (client.rs:494-499, 509-512): the service refuses the request and the model writes Terminated.
2 tasks done
bonk-ai Bot
added a commit
that referenced
this pull request
Oct 14, 2026
604 path:line citations on 15 hand-written pages pointed a few lines off after #600 and #601 moved their source lines. Each move is the doc line's blame-commit block found at HEAD by exact text and by git's own diff line mapping, or a read-only audit against the sentence for the 20 rows neither settled. state-machines.md's VmState section now says what ResumeRequested does after SuspendedVmEnded (client.rs:494-499, 509-512): the service refuses the request and the model writes Terminated. Co-authored-by: bonk-ai[bot] <269762587+bonk-ai[bot]@users.noreply.github.com>
3 tasks done
This was referenced Oct 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
An image can now set
AGENTD_DEFAULT_USERto the user that anexecorrun --execstart runs as when the request has nouser(#595). Before this, such a start always ran as the daemon's own user, which is root. A caller that left outuserhanded the workload guest root.userresolves it to a row of the guest's/etc/passwd, by name or by the uid its digits spell. The child gets that row's uid, primary gid, andHOME/USER/LOGNAME, soAGENTD_DEFAULT_USER=1000andAGENTD_DEFAULT_USER=agentrun the same way. Resolution happens per request, so a user that an earlier exec created resolves on the next start (AGENTD-22).userruns as given,rootand0included (AGENTD-23). An authorized caller can still ask for root. A request's own digits keep protocol 1's meaning: a bare uid in the daemon's group, with or without a row (AGENTD-16).usergets400 unknown_user(AGENTD-24). The detail namesAGENTD_DEFAULT_USER, and for digits it says the image needs a passwd row. It doesn't fall back to root or to a uid in root's group. The daemon still boots, and a start that sets its ownuserstill runs.PUT /v1/fs/tarandPUT /v1/fs/filecreate stays root's, as on main, so the default user can read the treerun . --execandcpupload but can't write into it. An exec that must write there runs withuser: 0, or the image or a root exec hands the tree over withchownfirst. PROTOCOL.md's "Upload ownership" section, TRUST.md, the guest Dockerfile guide and the changelog say so.userdoesn't read passwd.chownof/workspaceand a sync's in-guestrm -rfneed root, and both now senduser: 0. On an image that sets the variable, they'd otherwise run as its default user, where they can fail. So an image that sets the variable needs a client at this release or later, and the changelog says so.The default isn't reported on
/v1/health. In microvms-protocol 1.0.0, callers constructHealthfield by field. A new field failssemver:check(measured:constructible_struct_adds_field). Instead, the daemon logs the value at startup to the image's log group. The only wire schema change is theStartRequest.userdescription.The live conformance suite doesn't cover AGENTD-22..24, which waive the live layer: the suite's images don't set a default, and an image that did would change who every other live exec runs as. A local container run of the release binary covers the numeric default instead (see Evidence).
Size: 546 changed lines of product code, 363 of them unit tests in the files this edits. The setting, its per-request resolution to a passwd row, the refusal, and the
user: 0pins belong in one change. Without the row's gid, a numeric default runs workloads in root's group. Without the pins, any image that sets the variable runs the agent recipe'schownand a sync's deletions as a non-root user.Evidence
mise run checkpasses on 7558ada in 230 s: 1,813 Rust tests pass across 69 test binaries, 0 fail, and 28 carry#[ignore]. Prose ends at 0 errors and 4,077 warnings, and Vale reports no new warning on any edited doc against the merge base.cargo fmt --all -- --checkandcargo clippy --all-targets -- -D warnings, insidemise run check.cargo run -p agentd --bin schema -- --check: regenerated. OnlyStartRequest.user's description changed indocs/schema.json, and two--userhelp strings changed indocs/manifest.json.cargo build --release -p agentd --target aarch64-unknown-linux-muslLive container,
public.ecr.aws/amazonlinux/amazonlinux:2023-minimalwith the x86_64 musl release build of 7558ada's daemon as root,agent:x:1000:1000::/workspace:/bin/bashappended to/etc/passwd, andAGENTD_DEFAULT_USER=1000. Every exec below omitsuserunless shown:Restarted with
AGENTD_DEFAULT_USER=4242and no row for 4242:Mutation testing:
tools/check-mutants.py --diff(the scriptci:mutantsruns) over this branch's changed lines ofexec_start.rstested 6 mutants. It caught 2, 4 were unviable, and it missed none. Earlier, the first CI run failed shard 3 of 4 in cargo-mutants' unmutated baseline, becauseguards::parity::the_cli_answers_the_shared_case_corpus_against_its_fakesspawns microvms-core'sparity-tunnel-peerexample, which a mutant'scargo test -p microvms-clinever builds. a23b15b makesci:mutantsdepend onparity:fixtures, andmise run ci:mutants --base origin/main -- --shard 3/4 --in-placethen passed its baseline locally.mise run specandmise run spec:core.specexits 0 with no error findings. Warnings go from 102 to 112 against the merge base, all in the lint tier, all on the three new sentences.spec:coreexits 1 on oneGTWR_R26_ABSOLUTEerror ("never") in TRAP-4. That's inverify/spec/core.symspec.json, which this branch leaves alone, and its inputs are byte-identical to main's.Live conformance run: not run. See What and why.
Parity
microvms.pyi,index.d.ts).verify/parity/capabilities.tomldoesn't change, because no surface gains or loses a capability. The setting belongs to the daemon, which reads it from the image. Theuseroption keeps its type everywhere it appears:with_userin core,user=in Python, anduser?in Node.mise run parity:checkpasses.Guards
The merge base can't compile the new agentd and model tests, because they use
Config::default_user,Guest::default_user, and the model's new types. Those tests get hand-written faults inverify/guards/faults/default-user.tomlinstead. The microvms-app testsinstall_access_uploads_then_chowns_as_rootanda_hostile_deletion_path_is_refused_not_executeddo compile on the base, andmise run fail-to-pass -- --emit default-userwrote their entries. Their patches are regenerated against f410ec2.mise run guards:fire -- --only <id>over everydefault-userentry, on 7558ada with a clean tree, printed the lines below, thenguards: restored, every command passes again:Follow-ups
/v1/healthonce the protocol can take a field, either through asemver:checkallowance in a minor release or a#[non_exhaustive]Healthin 2.0.AGENTD_DEFAULT_USER, so the waived live layer has a check.CAP_SYS_ADMINand similar capabilities from the bounding set, is agentd: opt-in capability bounding-set drop after identity repair #599, with the design options weighed and the live measurements to take first.wrap_dockerfilecould setAGENTD_DEFAULT_USERfrom a task's lastUSER. A wrapped image would then run workloads as its author's user.ENOENT, becausecreate_hard_linkunlinks the destination before it links the source, which is the same entry. GNU tar writes one when a file is listed twice (tar -cf x.tar dir dir/file). This predates the change.Platform claims
Scope
(see
docs/STRATEGY.md).