Skip to content

release: bound sdist member enumeration in distribution verifier #215

Description

@seonghobae

Buyer-visible release-verification resource gap

Fresh protected main 7faf7a3b8a47980113982914000e724ab6a6cda5 still materializes the complete source-distribution member list in scripts/ci/verify_distribution.py before applying any semantic member-count ceiling. _verify_sdist(...) opens the gzip tar and calls TarFile.getmembers(), then builds a second full Python list of member names before _safe_archive_names(...) validates them.

This is distinct from #69's already-integrated bounded SBOM generator. That source repair is concentrated in scripts/ci/generate_release_sbom.py; the final distribution verifier remains a separate publication-admission path and can still allocate metadata proportional to attacker/build-output-controlled archive member count inside the existing compressed-file ceiling.

RCA

First failing boundary: release artifact verification, after the bounded archive snapshot is created but before sdist path/type/required-file validation.

Immediate cause: _verify_sdist(...) uses TarFile.getmembers() and then a list comprehension over every returned TarInfo.

Systemic cause: the distribution verifier has a compressed-byte ceiling and stable-source snapshot, but no independent finite semantic member-count ceiling and no streaming tar-member admission path. The earlier archive-enumeration hardening did not cover this separate verifier.

Falsifiable hypothesis: if sdist verification switches to a streaming gzip-tar reader, validates each member incrementally, and fails before accepting more than a documented finite member budget, the verifier no longer needs getmembers() or a second full member list while preserving canonical package checks.

Distinct remedies / feasibility

  1. Preferred: stream sdist members (r|gz), enforce a finite member ceiling before retaining each member, validate path and unsupported member types incrementally, and retain only the bounded name set needed for required/root checks. Python's supported tarfile streaming mode processes data as a stream of blocks and is available throughout the supported Python 3.10–3.14 matrix.
  2. Keep getmembers() and check len(...) afterward: rejected because the unbounded materialization has already occurred before the ceiling is enforced.
  3. Rely only on the 256 MiB compressed-file limit: rejected because highly compressible tar headers/member metadata can amplify object count and memory independently of the compressed-byte ceiling.
  4. Delegate verification to the SBOM generator: rejected because publication verification and SBOM generation are distinct evidence authorities and must each fail closed at their own parser boundary.

Acceptance criteria

  • Add immutable test-only RED evidence on exact protected main proving _verify_sdist(...) calls the full-list getmembers() path before semantic admission.
  • Add a deterministic over-budget archive regression proving the verifier stops at a finite member ceiling rather than accepting/materializing all members.
  • Switch semantic sdist verification to streaming gzip-tar iteration without random-access getmembers().
  • Reject the first member beyond the documented ceiling before retaining its name or continuing semantic verification.
  • Preserve unsafe-path, duplicate-path, link/device/special-member, versioned-root and required-file rejection; do not extract archive contents.
  • Preserve stable source snapshot/digest binding and exact checksum publication bytes.
  • Keep memory bounded by an explicit reviewed semantic member ceiling in addition to the compressed archive ceiling.
  • Preserve Python 3.10–3.14, Ruff, compileall, wheel/sdist/installed-package acceptance and exact 100% owned-production statement/branch coverage.
  • Pass exact-head CI/SAST and all live required workflow evidence. A Security Scan wrapper is non-passing while the actual pinned Dependency review step is skipped; unavailable/no-report Strix is likewise non-passing.
  • Merge only an unchanged gate-clean exact head and verify the verifier contract from protected main.

Non-goals

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions