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.
- 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.
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.
- 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:
- Import a disk image whose Dockerfile ends
USER 10001 and create a sandbox from it.
write_file("/data/run/inputs/main.py", "...", create_dirs=True).
exec("id -u") → 10001. exec("mkdir -p /data/run/out") → mkdir: cannot create directory '/data/run/out': Permission denied.
exec("stat -c '%u %a' /data /data/run /data/run/inputs") → 0 755 for every one.
mkdir("/data/other/deep") → 404 parent directory not found.
write_file("/data/run/f", "x", mode="777") then exec("stat -c '%u %a' /data/run/f") → 0 1411.
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 placedSummary
The Sandboxes data plane's file API creates every path it touches as
root:root 0755, whileexecruns as the sandbox image'sUSER. On an image with a non-zeroUSER, 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:mkdiris single-level, andmode=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
azure-containerapps-sandbox0.1.0b4 (Python 3.13), data plane at the api-version that SDK sendsUSER 10001, soexecreportsuid=10001(app)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 reportsuid=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>asuid=10001isPermission denied, because/is0:0 0755.2.
mkdiris single-level.mkdir("/<base>/x")where/<base>does not exist returns404 ... NotFound: parent directory not found, and so does any deeper chain. There is no parents/recursive flag, sowrite_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 asuid=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"andmode="0o733"are400validation 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, andmodecannot be used to hand a path to the guest even where it would otherwise help — the directorycreate_dirsmade alongside the file is still0:0 0755regardless.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;mvorln -sto swap a component →Permission denied. The control is/tmp, which is0o1777on 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.
write_fileandmkdiraccepting a uid/gid — or simply "the image'sUSER" — for the paths they create.mkdirwith parents, plus a mode that means what it says. Recursive creation and an octal-acceptingmode, so a caller creates the tree once as0o777or0o1777and the guest works inside it.USERby default. The service already knows which principalexecruns 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:
USER 10001and create a sandbox from it.write_file("/data/run/inputs/main.py", "...", create_dirs=True).exec("id -u")→10001.exec("mkdir -p /data/run/out")→mkdir: cannot create directory '/data/run/out': Permission denied.exec("stat -c '%u %a' /data /data/run /data/run/inputs")→0 755for every one.mkdir("/data/other/deep")→404 parent directory not found.write_file("/data/run/f", "x", mode="777")thenexec("stat -c '%u %a' /data/run/f")→0 1411.