fix(vision): bound the sandbox exec-read at the ingest cap

The container exec-read piped the whole file through base64 with no size
guard — the 50MB cap was only enforced host-side AFTER the full payload
had already streamed into host memory. A prompt-injected read of a huge
container file (or /dev/zero) could balloon the gateway process.

head -c (cap+1) bounds the read inside the sandbox; the +1 byte lets the
host distinguish at-cap from over-cap and reject with SourceTooLarge.
Input redirect replaces 'base64 --' (no argv exposure at all for
leading-dash paths). Docker integration tests re-verified live.
This commit is contained in:
teknium1 2026-07-04 15:12:49 -07:00 committed by Teknium
parent 6c068358e4
commit 0d27d2ed14
2 changed files with 35 additions and 5 deletions

View file

@ -277,16 +277,26 @@ async def _resolve_container_fallback(p: Path, ctx: ResolveContext, src: str) ->
f"session is available to read it",
src=src, origin="container")
# Bound the read INSIDE the sandbox: head -c caps at ingest-limit+1 bytes
# so a huge file (or /dev/zero) can't stream unbounded base64 into host
# memory — the +1 byte lets us distinguish "exactly at the cap" from
# "over the cap" after decode. The input redirect (< path) avoids argv
# entirely, so leading-dash paths can't be parsed as options; base64
# -w0 is GNU-only, so pipe through tr -d for BusyBox.
# env.execute is a blocking backend exec; keep it off the event loop so a
# multi-MB base64 read doesn't stall every other coroutine.
qp = shlex.quote(str(p))
res = await asyncio.to_thread(
env.execute, f"base64 -- {shlex.quote(str(p))} | tr -d '\\n'")
env.execute,
f"head -c {_MAX_INGEST_BYTES + 1} < {qp} | base64 | tr -d '\\n'")
if res.get("returncode", 1) != 0:
raise SourceNotFound(f"could not read '{p}' inside the sandbox", src=src, origin="container")
try:
data = base64.b64decode(res.get("output", ""), validate=True)
except Exception as exc:
raise NotAnImage(f"sandbox returned non-image data for '{p}': {exc}", src=src)
if len(data) > _MAX_INGEST_BYTES:
raise SourceTooLarge("image exceeds size limit", src=src, origin="container")
return _finalize(data, "", "container", src)