From bf565be0cede8fe515fd9e6bbd0f1152ed8d8aaf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 11 Jun 2026 01:42:12 +0000 Subject: [PATCH] docs: document sdists as opt-in with security (host RCE) and single-arch caveats Co-authored-by: Jason Hall --- pymage/README.md | 35 ++++++++++++++++++++-------- pymage/docs/real-world-comparison.md | 24 ++++++++++++------- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/pymage/README.md b/pymage/README.md index 05ba658..d726831 100644 --- a/pymage/README.md +++ b/pymage/README.md @@ -73,6 +73,7 @@ the config value, which overrides the built-in default. | `max-wheel-layers` | `--max-wheel-layers` | *(derived from `max-layers`)* | | `push-concurrency` | `--push-concurrency` | auto (≥ 4, scales with CPUs) | | `no-cache` | `--no-cache` | `false` (caching is on by default) | +| `build-sdists` | `--build-sdists` | `false` (sdists are opt-in; see warning below) | | `extras` | `--extra` (repeatable) | — (enables uv project optional-dependency groups) | | `package` | `--package` | — (build a single uv workspace member) | | `python` | `--python` | auto-detected from the base | @@ -166,18 +167,31 @@ get from `uv sync --no-dev`), not every package in the lock: Windows-only or stale-Python-only packages. Markers are evaluated per platform, so each arch of a multi-arch build gets the correct set. -### Source distributions (sdists) +### Source distributions (sdists) — opt-in -When the lock pins a package that has **no compatible wheel** (an sdist-only -release, or a compiled package for a platform without a published wheel), pymage -builds a wheel from the sdist using the host's `pip` (`pip wheel --no-deps`). -This requires a Python toolchain (`python3`/`python` with `pip`) on the build -host. +By default pymage installs **only pre-built wheels**. If the lock pins a package +with no compatible wheel, the build fails fast and tells you to either supply a +wheel via `--find-links` or opt into sdist building. -- **Pure-python sdists** build to a `py3-none-any` wheel and work for any target. -- **Compiled sdists** can only be built for the **host platform**; building such - a package for a different architecture fails with a clear compatibility error. - Prefer a base/lock that provides pre-built wheels for those. +Building from an sdist is **off by default** and must be enabled with +`--build-sdists` (or `build-sdists = true` in `[tool.pymage]`). When enabled, +pymage downloads the hash-verified sdist and builds a wheel with the host's `pip` +(`pip wheel --no-deps`), which requires a Python toolchain (`python3`/`python` +with `pip`) on the build host. It is a power-user feature with two important +caveats: + +- **Security: building an sdist runs arbitrary code from the dependency** on the + build host. An sdist's `setup.py` / build backend executes during the build, + and — unlike a `docker build` — pymage does **not** sandbox it. A malicious or + compromised dependency can therefore achieve code execution on your build + machine. Only enable this for locks you trust, ideally in an ephemeral/CI + environment, and prefer pre-built wheels (`--find-links`, or a registry that + publishes wheels) whenever possible. +- **Architecture: compiled sdists are single-arch.** A pure-python sdist builds + to a `py3-none-any` wheel that works on any target. A *compiled* sdist can only + be built for the **host** platform, so a multi-arch build that needs to build + such a package will fail the compatibility check for the non-host arch. Use a + base/lock that provides pre-built wheels for every target arch instead. Built wheels are cached (keyed by the sdist hash and target) so they aren't rebuilt on subsequent builds. @@ -241,6 +255,7 @@ base that advertises its version. | `--package` | Build a single uv workspace member by name (default: union of all members). | | `--cache-dir` | Cache root (default: `$PYMAGE_CACHE_DIR` or the per-user cache dir). Caches compressed layers, downloaded wheels, and base interpreter detection. | | `--no-cache` | Disable all caching (layers, downloaded wheels, interpreter detection). | +| `--build-sdists` | Allow building wheels from sdists when no compatible wheel exists. **Runs the dependency's build code on the host (no sandbox); single-arch for compiled packages.** Off by default. | | `--prefix` | install prefix / venv root (default `/app/.venv`). | | `--workdir` | image working dir and source destination (default `/app`). | | `--user` | image user, e.g. `65532`. | diff --git a/pymage/docs/real-world-comparison.md b/pymage/docs/real-world-comparison.md index f9e231d..c0f8b96 100644 --- a/pymage/docs/real-world-comparison.md +++ b/pymage/docs/real-world-comparison.md @@ -123,13 +123,21 @@ the worst case is one bucket rather than the whole environment.) 1. **(FIXED) Installed the whole `uv.lock`, including dev groups.** Caused 50–63% dependency bloat above. Now resolves the runtime closure. -2. **(FIXED) sdist-only dependencies.** `imgpush` previously failed on - `timeout-decorator==0.5.0`, which publishes no wheel. pymage now builds a - wheel from the sdist (`pip wheel --no-deps`) and feeds it into the existing - layer path; the build host needs `python`/`pip`. Pure-python sdists build to - `py3-none-any` (any target); compiled sdists build for the host platform only - (cross-arch builds of those still error clearly). `imgpush` now builds with 52 - runtime wheels including the sdist-built `timeout-decorator`. +2. **(FIXED, opt-in) sdist-only dependencies.** `imgpush` previously failed on + `timeout-decorator==0.5.0`, which publishes no wheel. With `--build-sdists` + (or `build-sdists` in `[tool.pymage]`) pymage builds a wheel from the sdist + (`pip wheel --no-deps`, host `python`/`pip` required) and feeds it into the + existing layer path; `imgpush` then builds with 52 runtime wheels including + the sdist-built `timeout-decorator`. It is **off by default and intentionally + a power-user feature** for two reasons: + - **Security:** building an sdist runs the dependency's own build code on the + host with no container isolation (pymage has no daemon to sandbox it), so a + malicious dependency could get code execution on the build machine. Without + the flag, a wheelless package fails fast with guidance to supply a wheel via + `--find-links` or opt in. + - **Single-arch:** compiled sdists can only be built for the host platform, so + enabling them doesn't make a compiled package multi-arch — pure-python + sdists build to `py3-none-any` and remain portable. 3. **No system/OS packages (by design — documented).** pymage installs Python wheels, not apt/apk packages. `imgpush` needs `libmagickwand` (for `Wand`) and `nginx`; those must come from the **base image**. Projects with system-library @@ -162,7 +170,7 @@ the worst case is one bucket rather than the whole environment.) After the runtime-closure fix and this round of work, **pymage is a smaller, faster, reproducible alternative to a uv Dockerfile** for the projects studied: -all three now build (including `imgpush`, via sdist building), with no daemon and +all three now build (including `imgpush`, via opt-in sdist building), with no daemon and no build tooling in the image, and incremental rebuilds re-upload only changed layers. The main remaining caveat is **runtime system libraries**, which must be provided by the base image (documented), plus the multi-platform default for