Skip to content

apt: control files are capped at 64 KiB and indexes are rendered one list at a time - #895

Merged
schmitch merged 2 commits into
mainfrom
claude/797-apt-index-memory-bound
Nov 4, 2026
Merged

schmitch merged 2 commits into
mainfrom
claude/797-apt-index-memory-bound

Conversation

@schmitch

@schmitch schmitch commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Fixes #797

What changed

  • Control file cap. MAX_CONTROL_BYTES (crates/formats/apt/src/control.rs) is 64 KiB instead of 1 MiB. deb.rs now refuses a larger control file with "the package's control file is larger than 64 KiB", and uploads and imports turn that into a 400 (both read packages through parse_package).
  • Indexes are streamed. FormatPlugin::index used to return a Vec<IndexFile>. It now hands each file to a sink (&mut dyn FnMut(IndexFile) -> Result<(), FormatError>). The host (crates/server/src/indexes/mod.rs) signs, checks and stores each file in the object store as it arrives, then drops its content, and records the generation in one transaction as before. apt renders one (component, architecture) list at a time: it builds the text, compresses it, hands over the plain, gz and xz files with their by-hash copies, and drops them before starting the next list. Memory now holds each stanza once plus one list's files, not every list's text, a copy and a by-hash clone at once.
  • The file limits are checked as files arrive. The paths, content types, MAX_INDEX_FILES and MAX_INDEX_BYTES that sign_and_check used to check once everything was in memory are now checked file by file (Bounds). An overrun stops the plugin through the sink's error.
  • Size check before rendering. IndexInput has a new field, max_bytes, which the host sets to MAX_INDEX_BYTES. Before making any list, apt adds up the stanza bytes of every list and doubles it, because each Packages is stored by name and by hash. If that is over max_bytes, it refuses without compressing anything.
  • Packages already stored over the cap. A package uploaded while the cap was 1 MiB keeps its stored control data. If that data is longer than MAX_LISTED_CONTROL_BYTES (2 × the cap), apt leaves it out of the indexes instead of failing or bloating every generation. It stays in the pool (and in browse) until it is deleted. The limit is twice the cap because the stored control data is a canonical respelling that can be a few bytes longer than the uploaded file, so no package accepted under the new cap is ever left out. apt is new in 2.2.0, so only deployments of the 2.2.0 development line can have such packages. The format crates have no logging, so nothing is logged when a package is left out. docs/apt.md documents the rule.

Tests

Changelog

docs/changes/2.2.0/797.fixed.md. docs/apt.md (Uploading packages) lists the new 64 KiB limit and says what happens to older, larger packages. docs/format-plugins.md (Signed indexes) describes the streaming index contract and max_bytes.

Not fixed here

  • A single list can still get as large as half of MAX_INDEX_BYTES (1 GiB), and it is held about twice while it is compressed and handed over. Reaching that takes some 8,000 packages at the cap in one list. apt: a writer can break a hosted repository's indexes for everyone with packages of many components and architectures #761 bounds the number of index files, not their size; a per-list bound would be a separate change.
  • As before this change, while a generation is being made the host keeps a committed object-store handle (temp file and lease) for every file it has stored, and generation is capped at MAX_INDEX_FILES files.

🤖 Generated with Claude Code

https://claude.ai/code/session_012e6DJYtQDk1mTgJAn2noU2

@schmitch
schmitch force-pushed the claude/797-apt-index-memory-bound branch from b53c757 to 017a61a Compare November 4, 2026 00:18
@schmitch
schmitch added this pull request to the merge queue Nov 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 4, 2026
A package's control file could be 1 MiB, and an index generation held
every list's text, a copy and a by-hash clone at once: a writer's tiny
packages with large descriptions OOM-killed the instance on every index
request.

- deb.rs refuses a control file over 64 KiB (400 on upload and import).
- FormatPlugin::index hands its files to a sink one at a time; the host
  signs, checks and stores each as it comes, so a generation holds one
  list and its copies, not the whole index.
- IndexInput::max_bytes: apt checks what its Packages lists will hold
  before it makes any.
- A package stored before with control data over twice the cap is left
  out of the indexes instead of failing them.

Fixes #797

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJYtQDk1mTgJAn2noU2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJYtQDk1mTgJAn2noU2
@schmitch
schmitch force-pushed the claude/797-apt-index-memory-bound branch from 017a61a to 23d9d19 Compare November 4, 2026 06:13
@schmitch
schmitch added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 8b54731 Nov 4, 2026
19 checks passed
@schmitch
schmitch deleted the claude/797-apt-index-memory-bound branch November 4, 2026 06:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

apt: a writer's packages with 1 MiB control files make an index generation hold gigabytes, which OOM-kills the instance on every index request

2 participants