Skip to content

fix(container): set tmpfs mode to 1777 for non-root container users#355

Open
abezzub-dr wants to merge 3 commits into
majorcontext:mainfrom
abezzub-dr:fix/tmpfs-perms
Open

fix(container): set tmpfs mode to 1777 for non-root container users#355
abezzub-dr wants to merge 3 commits into
majorcontext:mainfrom
abezzub-dr:fix/tmpfs-perms

Conversation

@abezzub-dr
Copy link
Copy Markdown
Contributor

@abezzub-dr abezzub-dr commented May 21, 2026

Summary

  • Set explicit TmpfsOptions{Mode: 0o1777} on tmpfs mounts produced by the Docker runtime. Without this, runc creates tmpfs as mode 755 owned by root, and any non-root container user (e.g., moatuser) gets EACCES writing into directories declared via mounts.exclude.
  • Add exec option to Docker tmpfs mounts to override runc default noexec flag. Without this, native binaries in excluded paths (e.g., turbo, esbuild in node_modules) fail with EACCES when spawned.
  • Extract the inline mount-building from DockerRuntime.CreateContainer into a buildContainerMounts helper so the behavior can be unit-tested without a live Docker daemon.
  • Apple Containers: switch from --tmpfs to --mount type=tmpfs,destination=...,mode=1777 so the mode is set explicitly. Apple --tmpfs flag passes empty options and does not support mode syntax. Apple containers do not need exec — the kernel defaults to allowing exec on user-supplied tmpfs mounts.
  • Unit tests covering: tmpfs mode + exec, bind mount conversion, bind-before-tmpfs ordering, and updated Apple BuildCreateArgs tests for the new --mount syntax.

Why

A user hit this with a pnpm install inside a moat container:

pnpm: EACCES: permission denied, mkdir '/workspace/.pnpm-store/v11'

Their moat.yaml had mounts.exclude: [node_modules, .pnpm-store, ...]. The exclude correctly produced tmpfs mounts at the right paths, but the tmpfs landed as root:root 755 — unwritable for the non-root container user, so pnpm could not even create its store directory.

Mode 1777 matches what Docker CLI --tmpfs flag uses by default, so the fix brings the SDK path in line with user expectations.

Additionally, even after fixing the mode, pnpm build failed because runc defaults tmpfs to noexec:

scripts/turbo.sh: line 11: /workspace/node_modules/.bin/turbo: Permission denied
Error: spawn .../@turbo/linux-arm64/bin/turbo EACCES

The exec option fixes this — excluded paths like node_modules contain native binaries that must be executable.

Test plan

  • go test -short -race ./... — all packages green
  • make lint — 0 issues
  • TestBuildContainerMounts_TmpfsWritableAndExec verifies mode 1777 and exec option
  • Apple TestBuildCreateArgs tests updated for --mount syntax
  • Manual: run a moat container with mounts.exclude: [node_modules] and confirm a non-root user can mkdir and execute binaries inside /workspace/node_modules

Out of scope

  • Configurability — kept 1777 hardcoded for now. If anyone wants per-mount mode control, a mode field on the exclude config can be added later without changing the default.

Tmpfs mounts created via the Docker SDK without explicit TmpfsOptions
inherit runc's default mode of 755 owned by root. Non-root container
users (e.g., the `moatuser` we run agents as) cannot write to such
mounts, so `mounts.exclude` directories like `/workspace/node_modules`
return EACCES on any write attempt:

  pnpm: EACCES: permission denied, mkdir '/workspace/.pnpm-store/v11'

Set mode 1777 (sticky world-writable) explicitly. This matches the
default that Docker's `--tmpfs` CLI flag uses, so behavior now lines up
with what users coming from `docker run --tmpfs /foo` expect.

The mount-building logic in CreateContainer was extracted to a
`buildContainerMounts` helper so the behavior is unit-testable without
a live Docker daemon.

Apple Containers (apple.go) likely has the same bug but is left for a
follow-up — the `container run --tmpfs` mode syntax needs verification
on a Mac host.
@abezzub-dr abezzub-dr marked this pull request as draft May 21, 2026 15:10
…ners

Docker: add "exec" to TmpfsOptions to override runc's default "noexec"
flag on tmpfs mounts. Without this, native binaries in excluded paths
(e.g., turbo, esbuild in node_modules) fail with EACCES when spawned.

Apple: switch from --tmpfs to --mount type=tmpfs,destination=...,mode=1777
so the mode is set explicitly. Apple's --tmpfs flag passes empty options
and does not support mode syntax.
…inerMounts signature

- Lift mode 1777 to a single package-level constant in runtime.go so docker
  and apple runtimes can't drift.
- Narrow buildContainerMounts to take ([]MountConfig, []TmpfsMount) instead
  of the full Config — function was only reading two fields.
- Reuse buildContainerMounts in dockerSidecarManager.StartSidecar, which
  had an identical bind-mount loop.
- Switch 0o1777 to 01777 to match the rest of the codebase's octal style.
- Trim the 9-line tmpfs comment to one line covering the non-obvious WHY
  (the exec option for native binaries); drop the "PR majorcontext#355" task reference
  in apple.go.
- Delete narrative WHAT-only doc comments from the new tests; test names
  already state what's asserted.
@abezzub-dr abezzub-dr marked this pull request as ready for review May 22, 2026 08:51
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