Skip to content

fix(sandbox): start OCI images with explicit UID or GID - #253

Open
yingdi-shan wants to merge 1 commit into
kvcache-ai:mainfrom
yingdi-shan:fix/oci-numeric-user-startup
Open

fix(sandbox): start OCI images with explicit UID or GID#253
yingdi-shan wants to merge 1 commit into
kvcache-ai:mainfrom
yingdi-shan:fix/oci-numeric-user-startup

Conversation

@yingdi-shan

@yingdi-shan yingdi-shan commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

What

Start and resume OCI images whose User config specifies a numeric UID or an explicit user/group pair. Resolve the identity to an envd account while preserving the requested UID/GID, image environment, and working directory.

Why

OCI permits numeric identities without a passwd entry, but envd requires an account name. Passing 12345 or 12345:23456 directly to envd can prevent image startup.

Related issue

Extracted from #247 alongside #252 (volume deletion) and #254 (BuildKit).

Scope and non-goals

Guest identity resolution, privileged internal filesystem maintenance, tests, and runtime-configuration documentation. BuildKit integration and volume-deletion optimization are separate PRs.

Design and behavior changes

  • Bootstrap authenticated envd as root before account resolution, then apply the final image configuration.
  • Reuse existing accounts when their UID/GID matches; otherwise add a distinct account without rewriting existing users or their primary groups. Reuse that account after restore.
  • Resolve named groups, reject invalid IDs or missing named identities, cap account-file reads at 1 MiB, and bound account setup.
  • Use the provided BusyBox and explicitly select root for internal filesystem operations independently of the image's default user.

Compatibility and operations

  • Public API or generated protocol: unchanged.
  • Configuration or defaults: unchanged; an unknown numeric UID without an explicit group uses GID 0.
  • Snapshot manifest, artifact layout, or storage format: unchanged. Added guest passwd entries are captured normally.
  • Upgrade and rollback: no host-state migration; existing guest accounts are preserved.
  • Host requirements, permissions, ports, or dependencies: unchanged; uses the existing tools drive.

Validation

  • make fmt
  • make clippy
  • make test-unit
  • Relevant Rust integration tests
  • Documentation updated

Commands and results:

make fmt clippy: passed
make test-unit: passed, including capability and ublk checks
cargo test -p agentenv --test integration numeric_image_users_preserve_identity_after_start_and_resume -- --nocapture: passed
git diff --check: passed

The VM test ran through the repository capability runner as a non-root user with isolated test state and dependency copies. Unit coverage includes existing and missing UIDs, explicit numeric and named groups, account-name collisions, invalid IDs, authenticated initialization, and image environment preservation. Go, code generation, and performance benchmarks do not apply.

Risks and reviewer notes

Review src/sandbox/envd/user.rs. The fix adds passwd entries only when needed and leaves existing accounts intact. The new VM test checks six identities across cold startup and resume without depending on BuildKit. Guest root-selection helpers overlap identically with the volume fix and merge cleanly.

Checklist

  • The PR contains one coherent change and no unrelated formatting or refactoring.
  • New behavior is covered by focused regression tests.
  • Logs and examples contain no credentials, tokens, or private registry information.
  • No generated code was manually edited.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 2 issue(s) in this PR.

  • ✅ Successfully posted inline: 2 comment(s)

Comment thread src/sandbox/envd/user.rs Outdated
Comment on lines +21 to +25
let groups = if user.contains(':') {
self.read_account_file("/etc/group").await?
} else {
String::new()
};

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.

bug · medium
Only a named group requires /etc/group, but this downloads it for every USER containing :, including numeric forms such as 1000:1000. Such a valid numeric identity can now fail solely because an otherwise irrelevant group file is non-UTF-8 or exceeds the 1 MiB limit. Parse the group component first and read /etc/group only when that component is nonnumeric.

Comment thread src/sandbox/envd/user.rs Outdated
Comment on lines +128 to +135
if let Some(fields) = existing {
if parse_id(fields[3])? == gid {
return Ok(ResolvedUser {
name: fields[0].to_owned(),
passwd_entry: None,
});
}
}

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.

bug · medium
An explicit USER user:group must ignore the account's other supplementary group memberships, even when the requested GID equals its existing primary GID. Returning the original username here lets envd resolve that name normally and can retain supplementary groups, diverging from Docker semantics (and potentially granting extra access). For any explicit group, use/reuse the separate generated alias rather than returning the original account solely because its primary GID matches; reserve this early return for group.is_none().

Comment thread src/sandbox/envd/user.rs
Comment on lines +150 to +159
if let Some(fields) = names.get(name.as_str()) {
if fields[2].parse::<u32>() == Ok(uid) && fields[3].parse::<u32>() == Ok(gid) {
return Ok(ResolvedUser {
name,
passwd_entry: None,
});
}
continue;
}
let passwd_entry = Some(format!("{name}:x:{uid}:{gid}::{home}:/bin/sh"));

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.

bug · medium
A pre-existing image account named aenv-{uid}-{gid} is treated as one generated by this code based only on UID/GID. If that account has a different home (or other relevant passwd fields), envd will use those values, so relative filesystem operations no longer preserve the requested account's home as intended. Only reuse an entry if it matches the complete generated entry (at least UID, GID, and home); otherwise continue to a free suffix. Add a test for a same-UID/GID collision with a different home.

Comment thread tests/integration/process.rs Outdated
Comment on lines +28 to +29
("nobody:0", "65534", "0"),
("12345:nogroup", "12345", "65534"),

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.

test · low
These expectations couple the identity test to distro-specific nobody/nogroup IDs, while the shared fixture uses a mutable image tag (ghcr.io/linuxserver/baseimage-ubuntu:noble). A valid account-database change could fail this test even though name resolution remains correct. Please either pin/use a fixture with guaranteed account mappings or determine the named account/group IDs from the guest before asserting them.

Comment on lines +39 to +42
if restored {
sandbox.pause().await?;
sandbox.resume().await?;
}

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.

test · low
This only pauses and resumes the same running VM, so envd is not restarted and EnvdInstance::init does not run again. It therefore does not exercise the snapshot-restore path where numeric USER resolution must be repeated/idempotent. Keep the returned snapshot and start a new sandbox with resume_from_snapshot_config (or equivalent) to cover restoration.

@yingdi-shan
yingdi-shan force-pushed the fix/oci-numeric-user-startup branch 2 times, most recently from 0cf2f83 to 10ef972 Compare September 9, 2026 05:42
@yingdi-shan
yingdi-shan force-pushed the fix/oci-numeric-user-startup branch from 10ef972 to 9078d89 Compare September 9, 2026 05:47
Comment thread src/sandbox/envd/user.rs
);
bytes.extend_from_slice(&chunk);
}
String::from_utf8(bytes).with_context(|| format!("guest {path} is not UTF-8"))

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.

bug · medium
This makes initialization fail for any numeric/UID:GID USER whenever either account file contains a non-UTF-8 byte, even if the requested UID/GID is represented by an otherwise valid ASCII record. Unix passwd/group files are byte-oriented and unrelated records can contain arbitrary bytes; numeric identity resolution only needs to parse the relevant colon-delimited fields. Please parse the account files as bytes (or use a carefully scoped lossy conversion) so unrelated invalid UTF-8 does not prevent a usable image from starting.

Comment thread src/sandbox/envd/user.rs
Comment on lines +103 to +104
.filter(|fields: &Vec<&str>| fields.len() == 7)
.collect();

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.

bug · medium
Malformed passwd records are silently dropped here. For a numeric UID this can make an existing account look absent and cause a second generated entry to be appended with a potentially different name/home, while for a named account it produces a misleading 'absent' error. Since this file is the source of identity resolution, please reject malformed records (with context) or parse them without silently discarding them.

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.

1 participant