Skip to content

Feature Request: Sandboxes file API to create path as user #1820

Description

Is your feature request related to a problem? Please describe.

Sandboxes file API creates every path as root:root 0755, so a non-root sandbox image cannot write anything the host placed

Summary

The Sandboxes data plane's file API creates every path it touches as root:root 0755, while exec runs as the sandbox image's USER. On an image with a non-zero USER, the combination means a guest program can create nothing inside a directory the host wrote into — so the ordinary shape of "upload a program and its inputs, exec it, read its artifacts back" does not work at all. No call available today closes the gap: mkdir is single-level, and mode= is parsed as a decimal integer (see below), so nothing a caller can send produces a directory the guest may write.

Related, and already filed: #1807 (stat/list carry no signal that distinguishes a FIFO from an empty regular file).

Environment

  • SDK azure-containerapps-sandbox 0.1.0b4 (Python 3.13), data plane at the api-version that SDK sends
  • Guest: an imported disk image built on the Azure Linux base with USER 10001, so exec reports uid=10001(app)
  • Everything below is one sandbox per run, disposed afterwards

What we measured

1. Every component the file API creates is root's. After write_file(path, content, create_dirs=True) for a path five levels deep, each created component reports uid=0 gid=0 mode=0o755 — including the leaf directory the guest program was expected to work in. Before any host write, the guest cannot start the chain either: mkdir -p /<base> as uid=10001 is Permission denied, because / is 0:0 0755.

2. mkdir is single-level. mkdir("/<base>/x") where /<base> does not exist returns 404 ... NotFound: parent directory not found, and so does any deeper chain. There is no parents/recursive flag, so write_file(create_dirs=True) is the only call that creates a tree — and it creates it as root.

3. mode= is parsed as decimal, which is a bug on its own. write_file(..., mode="777") is accepted and the file lands as uid=0 mode=0o1411 — setuid. 777 decimal is 0o1411, so the field looks like it is read as a base-10 integer and applied as a raw mode word. mode="0o777" and mode="0o733" are 400 validation errors, so there is no way to spell an octal mode at all. Two consequences: a caller who writes the conventional "777" silently gets a setuid file, and mode cannot be used to hand a path to the guest even where it would otherwise help — the directory create_dirs made alongside the file is still 0:0 0755 regardless.

4. The guest can do nothing inside that tree. As uid=10001, inside a chain the file API created: mkdir -p <dir>/w → Permission denied; writing a file into it → Permission denied; mv or ln -s to swap a component → Permission denied. The control is /tmp, which is 0o1777 on the same image and where all three succeed — so this is ordinary POSIX permissions on a root-owned tree, not a sandbox restriction of some other kind.

Why it matters

The workload this blocks is the common one: a host uploads a program and its inputs, execs it, and reads back what it wrote. Every artifact a guest program produces has to be created by the guest, inside a directory the host chose, and that is exactly what is refused. It also stops any supervisor pattern that writes a marker file from inside the guest — a launcher that records its own pid and exit code cannot create either.

A non-root image is what hardening guidance asks for, and it is the only kind this affects. So today the only way to run this shape on ACA Sandboxes is to run the guest as root, which is the opposite of the recommendation.

Describe the solution you'd like.

  1. A creating call that takes an owner. write_file and mkdir accepting a uid/gid — or simply "the image's USER" — for the paths they create.
  2. mkdir with parents, plus a mode that means what it says. Recursive creation and an octal-accepting mode, so a caller creates the tree once as 0o777 or 0o1777 and the guest works inside it.
  3. Create as the image's USER by default. The service already knows which principal exec runs as; making created paths owned by it would remove the split rather than paper over it.

Option 1 or 3 is what we would use. Option 2 works but leaves a world-writable tree where a narrower answer exists.

Describe alternatives you've considered.
Use less secure option - running under root user.

Additional context.

Repro:

  1. Import a disk image whose Dockerfile ends USER 10001 and create a sandbox from it.
  2. write_file("/data/run/inputs/main.py", "...", create_dirs=True).
  3. exec("id -u") → 10001. exec("mkdir -p /data/run/out") → mkdir: cannot create directory '/data/run/out': Permission denied.
  4. exec("stat -c '%u %a' /data /data/run /data/run/inputs") → 0 755 for every one.
  5. mkdir("/data/other/deep") → 404 parent directory not found.
  6. write_file("/data/run/f", "x", mode="777") then exec("stat -c '%u %a' /data/run/f") → 0 1411.

Activity

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

    Needs: triage 🔍Pending a first pass to read, tag, and assignenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions