Skip to content

Commit 76f5bda

Browse files
author
Roy Lin
committed
fix(cli,guest): support a named --user / exec -u, resolved in the guest
Docker accepts `--user name` / `exec -u name`, resolving it against the container's /etc/passwd; a3s-box rejected any non-numeric user with 'Named user is not supported yet'. normalize_user_part now forwards a named user/group as-is, and the guest exec server resolves it (resolve_named_user) against the container root — defaulting the rootfs to '/' for an exec (no rootfs override), so `exec -u nobody` runs as 65534 like Docker. (The run path already resolved the image USER in-guest; this also makes `run --user name` work.)
1 parent e4b27e1 commit 76f5bda

3 files changed

Lines changed: 36 additions & 20 deletions

File tree

src/cli/src/boot.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -497,13 +497,13 @@ mod tests {
497497
}
498498

499499
#[test]
500-
fn test_config_from_record_rejects_invalid_user() {
500+
fn test_config_from_record_accepts_named_user() {
501+
// Named users are forwarded as-is and resolved in the guest.
501502
let mut record = sample_record();
502503
record.user = Some("node".to_string());
503504

504-
let err = config_from_record(&record).unwrap_err();
505-
506-
assert!(err.contains("Named user"));
505+
let config = config_from_record(&record).unwrap();
506+
assert_eq!(config.user.as_deref(), Some("node"));
507507
}
508508

509509
#[test]

src/cli/src/commands/common.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub struct CommonBoxArgs {
5151
#[arg(long)]
5252
pub hostname: Option<String>,
5353

54-
/// Run as a specific user (supported: root, UID, UID:GID)
54+
/// Run as a specific user (root, UID, UID:GID, name, or name:group)
5555
#[arg(short = 'u', long)]
5656
pub user: Option<String>,
5757

@@ -413,12 +413,18 @@ fn normalize_user_part(part: &str, label: &str, original: &str) -> Result<String
413413
"Invalid --user '{original}' ({label} component is empty)"
414414
));
415415
}
416+
if part.contains('\0') {
417+
return Err(format!(
418+
"Invalid --user '{original}' ({label} contains a NUL byte)"
419+
));
420+
}
416421
if part == "root" {
417422
return Ok("0".to_string());
418423
}
419-
part.parse::<u32>().map(|_| part.to_string()).map_err(|_| {
420-
format!("Named {label} '{part}' is not supported yet; use root or a numeric UID[:GID]")
421-
})
424+
// Numeric stays numeric; a named user/group is forwarded as-is and resolved
425+
// in the guest against the container's /etc/passwd and /etc/group, like
426+
// Docker's `--user name`.
427+
Ok(part.to_string())
422428
}
423429

424430
/// Validate an in-guest working directory override.
@@ -890,13 +896,22 @@ mod tests {
890896
}
891897

892898
#[test]
893-
fn test_validate_runtime_options_rejects_named_user() {
899+
fn test_validate_runtime_options_accepts_named_user() {
900+
// A named user is forwarded as-is and resolved in the guest against the
901+
// container's /etc/passwd (like Docker's --user name).
894902
let mut args = default_common_args();
895903
args.user = Some("node".to_string());
896-
897-
let err = validate_runtime_options(&args).unwrap_err();
898-
899-
assert!(err.contains("Named user"));
904+
validate_runtime_options(&args).unwrap();
905+
assert_eq!(
906+
normalize_user_option(Some("nobody")).unwrap().as_deref(),
907+
Some("nobody")
908+
);
909+
assert_eq!(
910+
normalize_user_option(Some("node:staff"))
911+
.unwrap()
912+
.as_deref(),
913+
Some("node:staff")
914+
);
900915
}
901916

902917
#[test]

src/guest/init/src/exec_server.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -438,13 +438,14 @@ fn build_command(
438438
};
439439
let timeout = Duration::from_nanos(timeout_ns);
440440
let workdir = spec.working_dir.unwrap_or("/");
441-
// Resolve a named user (CRI RunAsUserName) against the container's
442-
// /etc/passwd before numeric parsing; falls through to spec.user when the
443-
// value is already numeric/root or cannot be resolved.
441+
// Resolve a named user (CRI RunAsUserName, or `exec -u <name>`) against the
442+
// container's /etc/passwd before numeric parsing; falls through to spec.user
443+
// when the value is already numeric/root or cannot be resolved. For an exec
444+
// (no rootfs override) the container root is the current `/`.
445+
let resolve_rootfs = spec.rootfs.unwrap_or("/");
444446
let resolved_user = spec
445447
.user
446-
.zip(spec.rootfs)
447-
.and_then(|(user, rootfs)| crate::user::resolve_named_user(user, rootfs));
448+
.and_then(|user| crate::user::resolve_named_user(user, resolve_rootfs));
448449
let mut process_user = match parse_process_user(resolved_user.as_deref().or(spec.user)) {
449450
Ok(process_user) => process_user,
450451
Err(error) => {
@@ -458,9 +459,9 @@ fn build_command(
458459
// When a user is set without an explicit group (RunAsUser, no RunAsGroup),
459460
// default the primary gid to the user's /etc/passwd group — matching how a
460461
// normal login derives the primary group — instead of inheriting root's.
461-
if let (Some(process_user), Some(rootfs)) = (process_user.as_mut(), spec.rootfs) {
462+
if let Some(process_user) = process_user.as_mut() {
462463
if process_user.gid.is_none() {
463-
process_user.gid = crate::user::primary_gid_for_uid(rootfs, process_user.uid);
464+
process_user.gid = crate::user::primary_gid_for_uid(resolve_rootfs, process_user.uid);
464465
}
465466
}
466467

0 commit comments

Comments
 (0)