Repository navigation
Description
Summary
Files that an image layer assigns to a non-root owner show up as owned by uid 0 inside a gVisor actor. For example, a home directory created with useradd and chown -R in the Dockerfile appears as root-owned 0700, so the workload user cannot read its own home. Files created at runtime, and the DurableDir mount, show their correct owners, so the loss is specific to image-layer content.
Impact
- Who hits it: any integrator whose image gives image content to a non-root user, such as a home directory, an application directory or a cache.
- What breaks: the non-root workload cannot read or write its own image-provided files. Every image path meant to be non-root-owned stays root-owned.
- Cost: a root-privileged chown pass at every actor start.
- Severity: High.
- Proposed priority: P1. It breaks standard image conventions for non-root workloads. The workaround needs root and only covers paths the integrator knows about.
Environment
- Substrate
d277088b(v1alpha). - GKE 1.36.x.
- gVisor sandbox class, unprivileged WorkerPool workers.
Steps to reproduce / evidence
-
Build the image (verified live) with these lines:
RUN useradd -u 1000 -m agent && chown -R agent:agent /home/agentUnder Docker,
/home/agentand its contents are owned by1000:1000. -
Check inside the actor (verified live). Create an actor from a template using that image, then run:
$ stat -c '%a:%U:%G %n' /home/agent 700:root:root /home/agentEvery image-layer file under it (for example
.bashrc) also appears as uid 0. -
Control (verified live). Two kinds of file keep their correct owner (uid 1000):
- files the workload creates at runtime inside the actor;
- the 9p-mounted DurableDir workspace.
-
Candidate cause 1: the layer unpacker (code-read at
d277088b; not verified live). Image layers are unpacked byunpackLayerininternal/imagecache/unpack.go. It is called frominternal/imagecache/imagecache.go:712, and the image cache is created in atelet atcmd/atelet/main.go:206.unpackLayernever applies the tar header'sUid/Gid:unpack.gohas noLchown/Chowncall.- Regular files are created with
root.OpenFile(name, …, mode), and directories withroot.Mkdir(name, mode|0o700)(unpack.go:188and:207). Both belong to the extracting process, which runs as uid 0. - By contrast,
internal/tarutildoes restore ownership (restoreMeta→lchownEntry,tarutil.go:450-454andowner_linux.go:76-84). At this commit, though,tarutilis used for DurableDir and rootfs-upper snapshot archives, not for image layers.
-
Candidate cause 2: identity mapping or overlay presentation (hypothesis, not confirmed).
- The runsc gofer maps a full-range identity in a user namespace (the comment above
ateomGvisorCapabilities,cmd/atecontroller/internal/controllers/workerpool_apply.go:327-333). - Overlay presentation or copy-up of lower-layer files could also contribute.
If candidate 1 holds, these do not need to be involved.
- The runsc gofer maps a full-range identity in a user namespace (the comment above
Expected vs actual
- Expected: image-layer files keep the uid/gid recorded in the layer tar, as under Docker, containerd and Kubernetes.
- Actual: image-layer files appear as uid 0 inside the actor. Runtime-created files and DurableDir content are unaffected.
Workaround
At actor start, while still uid 0, an integrator can re-chown the uid-0 entries under the workload user's $HOME (and any other paths it knows should be non-root) to the workload user. An fd-based walk with no-follow semantics is advisable. Paths the integrator does not know about stay root-owned.
Additional detail
- Suggested fix: restore the tar header ownership of image-layer entries, then confirm inside a sandbox that the gofer and overlay present the host-side owners unchanged.
- The comment at
unpack.go:103-107says atelet unpacks "as plain root … without CAP_DAC_OVERRIDE". The atelet container drops all capabilities, so it has no CAP_CHOWN (see atelet drops all capabilities, so it cannot remove a DurableDir…). - The
whiteoutSetmetadata persisted per layer (unpack.go:40-90) records no ownership either. - So the fix is probably one of these: grant atelet CHOWN (and FSETID, if special bits are restored after the chown); or record the owners at unpack time and apply them in a context that already has CHOWN, as the ateom worker does.
- In either case apply
LchownbeforeChmod, astarutil.restoreMetadoes, because chown clears setuid/setgid.
- The comment at
- Quick diagnostic:
stata known non-root file in the host-side extracted layer directory (under atelet's image-cache directory). This separates an extraction-time loss (candidate 1) from a presentation-time loss (candidate 2). - Related issues:
- Actor rootfs drops setuid, setgid and sticky bits from image layers (
/tmpis 0777) (actor-rootfs-drops-special-mode-bits.md), which points at the same unpacker. - Actor rootfs
/is mode 0700, so non-root users cannot traverse the root filesystem (actor-rootfs-root-dir-mode-0700.md).
- Actor rootfs drops setuid, setgid and sticky bits from image layers (
Activity
BenTheElder commentedon Oct 7, 2026
atelet has CAP_DAC_OVERRIDE now, I agree we should fix this.
fw, ax: the tool user gets a home of its own, and the ax boot opens t…
Run containers as the image's USER and WORKDIR, with layer file owner…
[release-1.0] Cherry-pick #2392: Run containers as the image's USER a…