Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion scripts/ci/pingora_edge_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
MAX_FILE_BYTES = 1_048_576
MAX_RESPONSE_BYTES = 16_777_216
MAX_BLOB_BYTES = 100_000_000
# Decoded-pixel bound for PNG validation, separate from the HTTP response cap:
# a valid 2238x2052 RGBA screenshot decodes to 18.4 MB. 128 MiB covers 16-bit
# RGBA up to 4K and 8-bit RGBA up to 6K while keeping zlib output bounded.
MAX_PNG_DECODED_BYTES = 134_217_728
REPOSITORY_RE = re.compile(r"^(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$")
SHA_RE = re.compile(r"^[0-9a-f]{40}$")
# A base ref threaded into evaluate_pull_request may be either a branch name
Expand Down Expand Up @@ -833,7 +837,7 @@ def _is_complete_png(raw: bytes) -> bool:
pass_height = (height - y_start + y_step - 1) // y_step
row_bytes = (pass_width * channels * bit_depth + 7) // 8
expected_size += pass_height * (row_bytes + 1)
if expected_size > MAX_RESPONSE_BYTES:
if expected_size > MAX_PNG_DECODED_BYTES:
return False
scanlines.append((pass_height, row_bytes, pass_width))
decoder = zlib.decompressobj()
Expand Down
29 changes: 28 additions & 1 deletion tests/test_pingora_edge_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,33 @@ def chunk(kind: bytes, data: bytes) -> bytes:
assert not policy._is_complete_png(signature + header + chunk(b"TEXT", b""))


def test_png_decoded_size_is_not_bounded_by_the_http_response_cap() -> None:
"""A valid 2238x2052 RGBA screenshot decodes past 16 MiB and is still a PNG.

Regression for late-life-anxiety-reanalysis#257: five macOS window
screenshots under a declared artifact prefix were rejected, then reported
as "not valid UTF-8", because the decoded-pixel bound reused the HTTP
response cap.
"""

def chunk(kind: bytes, data: bytes) -> bytes:
payload = kind + data
return len(data).to_bytes(4, "big") + payload + zlib.crc32(payload).to_bytes(4, "big")

width, height = 2238, 2052
decoded = (b"\0" + b"\0" * (width * 4)) * height
assert len(decoded) > policy.MAX_RESPONSE_BYTES
header = width.to_bytes(4, "big") + height.to_bytes(4, "big") + bytes((8, 6, 0, 0, 0))
raw = (
policy.PNG_SIGNATURE
+ chunk(b"IHDR", header)
+ chunk(b"IDAT", zlib.compress(decoded))
+ chunk(b"IEND", b"")
)
assert len(raw) < policy.MAX_FILE_BYTES
assert policy._is_complete_png(raw)


def test_png_semantic_validation_fails_closed() -> None:
"""CRC-valid chunks still need a valid bounded PNG image stream."""

Expand Down Expand Up @@ -865,7 +892,7 @@ def indexed_png(
assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\0")), end))
assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\0\0\0\0\0") + b"x"), end))
assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\5\0\0\0\0")), end))
huge = (policy.MAX_RESPONSE_BYTES).to_bytes(4, "big") + (1).to_bytes(4, "big") + bytes((8, 6, 0, 0, 0))
huge = (policy.MAX_PNG_DECODED_BYTES // 4).to_bytes(4, "big") + (1).to_bytes(4, "big") + bytes((8, 6, 0, 0, 0))
assert not policy._is_complete_png(png(huge, image, end))

adam7 = (8).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 1))
Expand Down
Loading