Skip to content

Actor rootfs / is mode 0700, so non-root users cannot traverse the root filesystem #2035

Open

Description

@ptone

Summary

Inside a gVisor actor, the root directory / is root:root 0700. A workload that drops to a non-root user cannot resolve any path at all, not even /bin/sh, because the first path component is not traversable. Docker and containerd keep the image root's mode (normally 0755). As shipped, any non-root workload fails on Substrate.

Impact

  • Who hits it: every integrator whose workload runs as, or switches to, a non-root user. Agent runtimes and most hardened images do this.
  • What breaks: su, runuser and any exec as a non-root uid fail with Permission denied. The workload cannot start.
  • Cost: each integrator has to add a root-privileged fixup at actor start.
  • Severity: High.
  • Proposed priority: P1. It blocks non-root workloads out of the box. A workaround exists but needs root at startup, which depends on the uid-0 start (see Workload has no user field or no-new-privileges setting…).

Environment

  • Substrate d277088b (v1alpha).
  • GKE 1.36.x.
  • gVisor sandbox class, unprivileged WorkerPool workers.

Steps to reproduce / evidence

  1. Create the actor (verified live). Use an ActorTemplate whose image is a Debian-based image with a non-root user (for example useradd -u 1000 agent). Create an actor from it.
  2. Check the root mode inside the actor, as root (verified live):
    $ stat -c '%a:%U:%G %n' /
    700:root:root /
    
  3. Switch to the non-root user (verified live):
    $ su agent -s /bin/sh -c id
    su: failed to execute /bin/sh: Permission denied
    
    /bin/sh is itself 0755. The failure is at the traversal of /.
  4. Show that a 0755 root is sufficient (verified live). After chmod 755 / (as root), su and runuser to the non-root uid both succeed. The same test also chowned the user's home directory; the exec itself (su … -c id) depends only on / being traversable.
  5. Cause (code-read at d277088b):
    • internal/imagecache/bundle_linux.go:61-66 creates <bundle>/rootfs, upper and work with os.MkdirAll(d, 0o700).
    • The overlay is then mounted at rootfs with that upperdir (bundle_linux.go:81).
    • The merged overlay root takes its attributes from the upperdir root.
    • applyDirFixups in internal/imagecache/implicitdirs.go repairs implicit parent directories. It does not appear to cover / itself.

Expected vs actual

  • Expected: the actor's / has the image root's mode (0755 for standard base images), as under Docker, containerd and Kubernetes.
  • Actual: / is 0700 and owned by root. No non-root uid can traverse it.

Workaround

At actor start, while still uid 0, an integrator can:

  1. chmod 755 / if / is more restrictive than that.
  2. Check that the workload user can traverse the rootfs (for example that /bin/sh resolves).
  3. Fail closed if it cannot.

This only works because the workload starts as root.

Additional detail

  • Suggested fix: create the bundle rootfs/upperdir root with 0755, or copy the image root's mode onto it after the overlay is mounted.
  • Related issues (same class, image metadata lost in the actor rootfs):
    • Image-layer file ownership appears as uid 0 inside the actor (image-layer-file-ownership-lost.md).
    • Actor rootfs drops setuid, setgid and sticky bits from image layers (/tmp is 0777) (actor-rootfs-drops-special-mode-bits.md).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions