From c6ee1c70a8013bcc2821b57ff6e8df4222bed73f Mon Sep 17 00:00:00 2001 From: Manuel Huber Date: Fri, 26 Jun 2026 00:04:17 +0000 Subject: [PATCH] genpolicy: test image user group handling Add unit coverage for image config User values that include a group component. For Kubernetes, containerd CRI ImageStatus exposes only the user side before kubelet creates the container security context, so genpolicy keeps treating those values like the user-only form. The fixture uses in-memory passwd and group data so the test does not rely on private reproducer images. Signed-off-by: Manuel Huber Assisted-by: OpenAI Codex --- src/tools/genpolicy/src/registry.rs | 63 ++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/tools/genpolicy/src/registry.rs b/src/tools/genpolicy/src/registry.rs index 501611b8db..92bf767b81 100644 --- a/src/tools/genpolicy/src/registry.rs +++ b/src/tools/genpolicy/src/registry.rs @@ -348,8 +348,14 @@ impl Container { * 2. Contain only a UID * 3. Contain a UID:GID pair, in that format * 4. Contain a user name, which we need to translate into a UID/GID pair - * 5. Contain a (user name:group name) pair, which we need to translate into a UID/GID pair - * 6. Be erroneus, somehow + * 5. Contain a user name:group name pair + * 6. Be erroneous, somehow + * + * For Kubernetes, containerd CRI ImageStatus strips any group component + * before kubelet maps the image user into a CRI security context. Keep + * genpolicy aligned with that path: USER user:group behaves like USER + * user, and USER uid:gid behaves like USER uid. Direct ctr/crictl paths + * can resolve the group component differently because they bypass kubelet. */ if let Some(image_user) = &docker_config.User { if !image_user.is_empty() { @@ -766,3 +772,56 @@ fn parse_group_file(group: &str) -> Result> { Ok(records) } + +#[cfg(test)] +mod tests { + use super::*; + + fn container_with_image_user(user: &str) -> Container { + Container { + image: "test-image".to_string(), + config_layer: DockerConfigLayer { + config: DockerImageConfig { + User: Some(user.to_string()), + ..Default::default() + }, + ..Default::default() + }, + passwd: + "root:x:0:0:root:/root:/bin/sh\nwww-data:x:33:33:www-data:/var/www:/sbin/nologin\n" + .to_string(), + group: "root:x:0:\nwww-data:x:33:\nstaff:x:50:\nwheel:x:10:\n".to_string(), + } + } + + #[test] + fn image_user_group_component_matches_kubernetes_path() { + let cases = [ + "33:10", + "33:wheel", + "www-data:50", + "www-data:staff", + "www-data:thisgroupdoesnotexist", + ]; + + for image_user in cases { + let container = container_with_image_user(image_user); + let mut process = policy::KataProcess::default(); + + container.get_process(&mut process, false, false); + + assert_eq!(process.User.UID, 33, "image user: {image_user}"); + assert_eq!(process.User.GID, 33, "image user: {image_user}"); + assert_eq!( + process + .User + .AdditionalGids + .iter() + .copied() + .collect::>(), + vec![33], + "image user: {image_user}" + ); + } + } +}