fix(assets): a remote reference is inert and a declared size is bounded

Two MAJOR findings of the independent v0.10.0 review, both with the
shipped defaults, both new in 0.10.0. Repros rebuilt as tests first.

- A remote <img src>/xlink:href became a LIVE markdown image link in the
  persisted concept, with the address and query string chosen by whoever
  wrote the document. Extraction opens no socket; a consumer rendering
  the bundle does. Now inert text with the address in a code span,
  pinned by a property over the readers rather than by one string. The
  tier asymmetry (user-upload refuses, trusted-source persisted) went to
  the guard repo with the repro.
- Nothing bounded a declared image size: 9.6 KB of PDF declaring
  3000x3000 grayscale zeros took 83 MB peak RSS, linear in pixels.
  MAX_IMAGE_PIXELS (40 000 000) and MAX_IMAGE_BYTES (256 MiB) are read
  off the corpora (largest measured 18.6 MP on K2, 1.4 MP on R761) and
  checked on what the container declares, before any decompression;
  over them is asset_too_large, counted. The same bound closes the
  inline data: URI, which the review flagged and did not measure.

Also fixed, added by PM to this order: an inline PDF image was named
from id() of a Python object, so two concept files of the reference
corpus differed between builds. It is now named from its position.

R761 unchanged: 50 carried of 50 found, assets diff -rq clean.

Report: docs/2026-09-17-bildestien-0-10-1.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-18 00:32:50 +02:00
commit 230d1cbccd
8 changed files with 580 additions and 3 deletions

View file

@ -45,6 +45,8 @@ from xml.etree.ElementTree import Element
from .assets import (
AssetRejection,
ExtractedImage,
check_payload,
check_size,
encode_png,
read_image,
render_block,
@ -473,6 +475,16 @@ class _AssetCollector:
def _data_uri(self, match: re.Match[str], *, label: str | None) -> str:
payload = match.group("payload")
name = f"data-uri{Path(match.group('media').split('/')[-1] or 'bin').suffix or ''}"
# Before decoding: base64 expands by 3/4, and a percent-encoded payload
# by at most 1. An inline picture is small by nature, so a payload over
# the bound is refused unread -- the same bomb class as a PDF image
# declaring an enormous size. A refusal is a ROW, like every other.
try:
check_payload(
len(payload) * 3 // 4 if match.group("base64") else len(payload), name=name
)
except ExtractionError as exc:
return self.reject(name, code=exc.code, reason=str(exc), label=label)
try:
raw = (
base64.b64decode(payload, validate=True)
@ -1380,6 +1392,18 @@ def _pdf_image(stream: object, name: str) -> ExtractedImage:
"""
from pdfminer.pdftypes import resolve1
# THE DECLARED SIZE IS READ FIRST, and the stream is not touched until it
# is within the bound: `get_data()` decompresses, so a check after it has
# already paid for a picture of compressed zeros.
declared = dict(getattr(stream, "attrs", {}))
check_size(
resolve1(declared.get("Width")) if isinstance(resolve1(declared.get("Width")), int) else 0,
resolve1(declared.get("Height"))
if isinstance(resolve1(declared.get("Height")), int)
else 0,
name=name,
channels=1,
)
try:
data = stream.get_data() # type: ignore[attr-defined]
except Exception as exc:
@ -1454,7 +1478,14 @@ def _pdf_images(page: object) -> tuple[tuple[ExtractedImage, ...], tuple[AssetRe
# resource name local to one page's dictionary, so it is not an
# identifier -- the page number in front of it is what makes the string
# readable, and the content-addressed digest is what makes it unique.
label = str(drawn.get("name") or index).lstrip("/")
# A name pdfminer derived from `id()` is not a name. An inline image
# (`BI ... EI`) has no resource name, and pdfminer fills the field with
# the address of a Python object, so 0.10.0 wrote a pointer line that
# changed between two runs of one build -- two concept files of the
# reference corpus differed, which breaks the bit-exact rebuild.
# Measured 2026-09-17.
raw = str(drawn.get("name") or "").lstrip("/")
label = raw if raw and not raw.isdigit() else f"inline-{index}"
name = f"page-{number}-{label}"
stream = drawn.get("stream")
if stream is None: