* security(paint): cap distance-spaced steps per segment and procedural tip primitives
* test(paint): count steps through a Cell so the walker closure can be shared
* paint: keep the spacing carry-over when a segment ends at the step cap; lower the tip work budget
* security(compose): bound dense working sets and repeated group evaluation
Smart-filter mixing and stroke fitting work tile by tile instead of over the
union of sparse surfaces; render_layer admits its rect like render; hidden
layers no longer disable the proxy and uniform-area shortcuts; Blend If
groups nested deeper than two are judged by their result, so nesting costs
polynomial instead of exponential work; effect builds past an absolute byte
cap are skipped.
* CodeRabbit #8949: exact tile skips in filter_mask, allocated-tile scan in fit_stroke, contrasting backdrop in the nested-group test
- filter_mask mix/unmix: bare tiles (no tiles on either side) skip for
non-alpha formats too when the two surfaces' default pixels are equal;
differing defaults still get processed, so the clone's default pixel is
never left wrong under a non-unit mask
- shape_split fit_stroke: scan the union of the three surfaces' allocated
tiles (exact skip when all three defaults are transparent) instead of
every tile of a 300k-canvas document; non-transparent defaults fall
back to the full scan
- tests: the nested-groups assertion now uses a black backdrop so the
asserted white can only come from rendering through the group chain
fixes CodeRabbit review comments (major/minor) on filter_mask.rs:27,29,
shape_split.rs:126, tests.rs:2354
* compose: skip a BTreeSet in fit_stroke — a sorted Vec compiles under the wasm toolchain (E0599 on ::drain there)
* security(algo): bound working sets sized by caller-controlled extents
Zero-width inpaint grids, content-aware fill mosaics of turned thin images,
Liquify fields over huge canvases, tiny-scale reduction weights, warp tile
bookkeeping and per-tile source footprints, and windowed filters whose halo
follows an unclamped radius or a stacked Filter Gallery reach all asked for
memory sized by file or parameter input. Each is now bounded; halo
arithmetic saturates instead of overflowing.
* security(algo): budget variants by dimensions, size-check footprints first, widen the tile-halo maths
CodeRabbit follow-up:
- fill_cow settles the mosaic budget on variant dimensions only, so a
variant's image and mask buffers are not built when the mosaic would
exceed MAX_MOSAIC_PIXELS (complete falls back to the image itself).
- warp validates the footprint's pixel area against MAX_FOOTPRINT_PIXELS
before src.has_tiles_in, so an oversized footprint returns without the
tile-grid scan.
- auto_tile widens the input-derived halo arithmetic to i64 and clamps
before the cast, so an i32::MAX radius cannot overflow in debug builds
and the 2048 tile cap stands.
* fix(algo): drop the duplicate mosaic_size call left behind in fill_cow
The variant-budget rework kept both the new dimensions-based budget and
the old variants-based one; the old call no longer compiled after
mosaic_size started taking dimensions. Keep the comment that explains
the fallback.
* packaging: use [[ for bash conditional tests (SonarCloud S7688)
SonarCloud flags every bare [ conditional in bash scripts under packaging/ as a reliability smell (rule S7688). Convert the flagged [ ... ] tests to [[ ... ]]: safer word-splitting and no glob surprises, same semantics for these string and file tests.
* packaging/web: parse size overrides as base 10 under [[
CodeRabbit follow-up:
- data_end counts the scan-data bytes it crosses, and decode's
truncated-input bound is measured on those, not on bytes.len(): APP
padding padded before the scan used to charge the budget without
describing one more pixel.
- a regression test builds a truncated JPEG with ~256 KiB of complete
COMMENT padding whose patched 20000x20000 frame was admissible under
the file-length bound, and verifies padding alone cannot admit it
(while the same file at its real size still recovers).
- Limits::max_alloc docs state the animated WebP charging, next to the
PNG text/XMP sentence.
Rect::tile_count counts a rect's tiles in u64; try_take_tiles uses it (no usize overflow on 32-bit). Surface::translated reuses one rebuilt tile for equal source-tile windows. auto_slices merges vertically adjacent runs through a hash index instead of searching the output.
Reject zero-width fixed-record arrays, borrow pixel state arrays instead of
collecting them per occurrence, charge FiEf slots, curve records and layer
name bytes across the document, bound mixed-text expansion before appending,
and cap distinct allocation-table ids (deletion records now hold no Entry).
A truncated JPEG scan under a large declared frame was filled out by the
backend to the full admitted size; reject it when the file cannot plausibly
account for the pixels. Animated WebP frames each keep a copy of the ICC
profile, so the aggregate admission now counts it.
The generated droplet shim only needs to run as its creator, so 0700 keeps
other local accounts from reading the CLI path it invokes. The reversed
range in the arrow-key test is the case under test, not an accident.
🤖 Generated with Codebuff
Co-authored-by: Codebuff <noreply@codebuff.com>
* fix(vector): bound hostile geometry raster work
Guard geometry and tracing operations against malformed dimensions and degenerate inputs with bounded work and safe failures.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* fix(vector): address review (knot cap on split, relative identity tol, non-vacuous test)
* revert(geom): splitting past the quilt limit is intentional (smart_map test)
* fix(geom): cap identity tolerance at 1e-6 px
---------
Co-authored-by: Codebuff <noreply@codebuff.com>
* fix(text): bound font paths and imported text processing
Reject encoded path tricks and cap resource-heavy font and PSD text processing to keep untrusted inputs bounded.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* fix(text): reject percent-encoded DEL in served paths; satisfy clippy chunks_exact lint
---------
Co-authored-by: Codebuff <noreply@codebuff.com>
* fix(algo): avoid copying duplicate EXIF text payloads
Keep the first valid text value for each EXIF tag, preventing duplicate entries from repeatedly decoding and copying large attacker-controlled strings.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* test(exif): count text decodes so the duplicate-tag test observes skipped payloads
---------
Co-authored-by: Codebuff <noreply@codebuff.com>
The importer bounded and inflated the top-level .svgz once, then handed
the result to usvg::Tree::from_data, which re-checks the gzip magic and
decompresses again with no byte limit of its own if the content happens
to start with a nested gzip stream. The same default resolver decompresses
embedded image/svg+xml data URLs (SVGZ-in-data-URL) the same unbounded way.
Parse the already-bounded top-level text with Tree::from_str, which never
decompresses. Wrap the image-href resolver so a gzip-compressed embedded
SVG is inflated through our bounded inflate() first; the already-plain
bytes handed to the default resolver no longer start with the gzip magic,
so it parses them directly instead of decompressing a second time.
Finding: a6444163-e510-4925-ae63-727658b6582e (CWE-409)
Embedded PNG and WebP images reached resvg using only their displayed
destination area, while resvg/tiny-skia allocate and decode the full
intrinsic source image first. A highly compressed large image could
therefore trigger gigabyte-scale allocation and decompression outside
the importer's destination-memory accounting.
Wrap usvg's default image-href data resolver with a check that reads
PNG dimensions directly from the IHDR chunk and WebP dimensions via
photocraft_codecs::probe_size, rejecting images whose decoded RGBA size
exceeds the raster budget before resvg ever sees them.
Findings:
- f4fc2a9e-c26f-442d-9d05-4e793d6b0f13 (CWE-789, embedded WebP allocation)
- d765fad5-678e-4f15-a8f8-3e6ee086056a (CWE-789, embedded PNG allocation)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the civil_from_days copies behind the app's log timestamp, the
Measurement Log's date/time, and the perf report date with chrono as a
workspace dependency (reusing the version already in the tree). Output formats
are unchanged and pinned by the existing tests. AGENTS.md now says time goes
through chrono.
Follow-up to storytold/cadcraft#1785
* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed
chrono's From<SystemTime> unwraps internally, so a clock past year 262143
panicked. logging::timestamp and analysis_cmds::now_iso now read the duration
since the epoch and go through the fallible DateTime::from_timestamp, reading
an out-of-range or pre-epoch clock as the epoch as the old saturating code
did. All three ISO helpers (timestamp, now_iso, perf::iso_utc) write the year
with {:04} rather than %Y, which prefixes a '+' past year 9999.
Tests cover the far-future and year-10000 cases for timestamp and iso_utc.
* Don't build an unrepresentable SystemTime in the far-future test
UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.
* time: preserve old out-of-range date conversion (no epoch fallback)
`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any
instant past chrono's year range to 1970-01-01, changing output the old
hand-rolled Howard-Hinnant code converted exactly (e.g. 10_000_000_000_000 s
was 318857-05-20 17:46:40, not the epoch). Restore the old conversion domain
with a cycle-based helper that routes the calendar through chrono (the
calendar repeats every 400 years = 146_097 days, so month/day/time come from
chrono and only the year is carried in i128, keeping every i64 exact).
- apps/photocraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/engine/src/analysis_cmds.rs: extract pure `iso_from_unix` (cycle
based) from `now_iso` so the conversion is tested directly.
- xtask/src/perf.rs: `iso_utc` cycle-based, accepts the full u64 range the
old code did.
Pinned old outputs in tests:
logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch),
10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
iso_from_unix 10_000_000_000_000 -> 318857-05-20T17:46:40Z,
-10_000_000_000_000 -> -314918-08-13T06:13:20Z, i64 extremes
iso_utc u64::MAX -> 584554051223-11-09T07:00:15Z (was epoch),
10_000_000_000_000 -> 318857-05-20T17:46:40Z
* Don't build a year-318857 SystemTime unchecked in the logging test
Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.
* Name the 400-year range extension as the one exception to the time rule
The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.
* Say the library computes month and day; time-of-day is plain arithmetic
Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
decode_comps matched each nonzero layer-setting comp ID against the comp
vector with iter_mut().find(), an O(n*m) scan over two file-controlled
collections. A crafted PSD could cause quadratic CPU work during import.
Build a comp-ID-to-first-index map once and use it for setting lookups.
or_insert preserves first-match behavior for duplicate comp IDs; comp 0
(Last Document State) is still handled separately.
Finding: 47567ac0-a75f-4557-b127-ffc855e5bd8d (CWE-407)
parasites() implemented last-duplicate-wins by scanning all previously
collected names with iter_mut().find() per record. Parasite records are
file-controlled, so many distinct names caused O(n^2) CPU work before
unrecognized metadata was discarded.
Maintain a name-to-index map so replacement is O(1) while preserving
insertion order and last-value-wins behavior.
Finding: 6107f41e-9b17-4632-a9df-f88a74a796a8 (CWE-407)
Header::get scanned every card linearly, and axis resolution looks up
NAXIS1..NAXISn while find_image/data_len re-resolve the axes. Both the
commentary-card count and the NAXIS count are file-controlled, so a
crafted header multiplied them into quadratic header-processing work
without a correspondingly large decoded image.
Build a first-occurrence keyword index once in parse_header (skipping
HIERARCH cards, matching the prior filter) and use it in get().
Finding: eb2121d8-9dee-4422-bd57-f78dce60a61b (CWE-407)
actions.load gave each imported action a unique name with a helper that
restarted suffix numbering at 2 and linearly scanned all accepted names
for every candidate. A file repeating one base name many times caused
cubic comparison work during load.
Replace it with a UniqueNames helper backed by a HashSet for membership
and a per-base-name next-suffix cache, preserving the name, name 2,
name 3 ... sequence and first-match behavior.
Finding: 9c7a5fdb-131e-4687-8175-58d72007b0c5 (CWE-407)
Cryptomatte metadata enumeration had two file-driven amplifiers:
- cryptomatte_keys() tested key membership with iter().any(), O(n^2) in
the number of distinct cryptomatte/<key>/<field> attribute groups;
- layers() called layer_streams() for every key, rescanning all channels
and allocating a fresh owned stream list even when many keys name the
same layer.
Use a HashSet for key membership (keeping file order) and cache
layer_streams results by resolved layer name within each header, so
repeated keys reuse the discovered streams.
Findings:
- 120f4d51-b981-4fcc-abec-26f45f3efd29 (CWE-407, quadratic key membership)
- dae6e76b-a491-4089-892f-4cf34b911f36 (CWE-400, repeated per-key stream scans)
xmp_values advanced its cursor only past the matched key, so the
value-closing search (find(close)) rescanned the remaining packet for
every property-like substring. Repeating text such as
' tiff:ResolutionUnit>' inside the packet produced quadratic CPU work
even when no real resolution property existed.
Advance the cursor past each located value; a missing close delimiter
means the value is unterminated, so no later value can parse and the
scan stops. Valid multi-value extraction is unchanged.
Finding: 6ce55e33-27f0-4fb9-a22f-737382e1d328 (CWE-407)
The spelling list converted each misspelling's start and end byte offset
to a character offset with char_at, which recounts the text prefix from
the start every call. A layer with many short misspellings therefore
caused quadratic rescanning on the synchronous UI thread.
Collect the ascending offsets and map them to character offsets in a
single forward walk over the text. char_at is removed (byte_at still
handles the reverse direction in change()).
Finding: 392eb136-b836-4400-8ed7-f1e4549ff0ae (CWE-407)
fps_from_delays counted each frame's delay by scanning a growing vector
with iter_mut().find(), O(n^2) for a GIF with many distinct delays. A
crafted animation within the frame limit could stall the synchronous
import path.
Count delays in a HashMap and keep a first-seen order vector so the
most-common delay still wins, breaking ties toward the earliest-seen
delay exactly as before.
Finding: 23d54421-8e26-41b9-9690-ae1125ee2f85 (CWE-407)
Row 16 (9f6a3c79): preserve platform filename semantics in corpus scans
(no backslash-to-separator rewrite on Unix) and enforce destination
containment before copying files into the corpus directory.
Row 20 (6e2a985b): skip directory symlinks, dedupe visited directories by
canonical identity, and cap total directories/files so symlink cycles can
no longer drive exponential traversal.
Fixes findings 9f6a3c79-8bdf-4751-8eb4-87d3154074c9 and
6e2a985b-6b14-4b97-af07-7ca8333b9b75.
Object::get scanned the field vector linearly. An Affinity file can
define one large shared object and reference it repeatedly as sibling
layers, so repeated property lookups multiplied into quadratic CPU work
without the fields appearing more than once in the file.
Build a first-occurrence tag index when an object's fields are set
(via Object::new and set_fields, the latter covering the root object
that is filled after it is pushed). Positional fields (tag 0) stay out
of the index, and first-match semantics are preserved.
Finding: c315c4b0-dceb-459e-b39a-fa9c68ae6f40 (CWE-407)
subpath() looked up each polyline anchor's corner radius by scanning the
entire radius list, O(anchors * radii). A file supplying large anchor and
radius arrays could cause quadratic CPU work before geometry limits
inspected the path.
Build a record-to-maximum-radius map once and look each anchor up in it,
preserving the previous fold(0.0, f64::max) behavior for duplicate record
indices.
Finding: 89f65d65-f637-44fc-9a81-a9039a413a4b (CWE-407)
Reader::children re-collected a group's child objects by scanning every
raw Chld array slot, discarding nulls before the node budget counted
anything. A shared group holding a large null-filled child array, linked
many times, multiplied into excessive CPU work the MAX_NODES limit never
observed.
Charge every raw child slot (nulls included) against a document-wide
MAX_CHILD_SLOTS budget before filtering, via a new Stream::array_len
helper, and return a limit error when it is exhausted.
Finding: 3973fcd0-acda-4312-b538-d986dcffeb1c (CWE-400)
FilterEffects::parse tested the trailing bytes with
rest.iter().all(|b| b == 0) before checking rest.len() <= 3, so every
loop iteration scanned the whole remaining buffer. A block with many
empty sections produced quadratic CPU work.
Reorder the short-circuiting && so the three-byte length check gates the
zero scan, making each padding check constant work. Behavior is
otherwise identical.
Finding: 4887ae49-cfda-43b0-9296-a1816fc1ddba (CWE-407)
auto_slices tested every grid cell against the full slice-rectangle list
(covered(x, y) = taken.iter().any(...)). With slice edges forming the
grid, that is cubic in the slice count, so a crafted document's slice
arrangement could stall ordinary canvas drawing on the UI thread.
For each row band, mark covered x-cells in a single pass over the rects
that span the band, then scan the precomputed cells. This drops the inner
work from O(rects) per cell to O(1), while producing the same tiling.
Finding: 9fbea43b-3096-4cdc-b005-86d44a8fe57e (CWE-400)
Two per-pixel/per-sample amplifiers driven by a crafted ICC profile:
- table_inverse recomputed a sampled curve's flat-run edge indices by
scanning the whole table on every call, and transform construction
inverts each destination curve OUT_TABLE times. Compute the edges once
per curve via a new Curve::inverse_table batch method.
- eval_multilinear enumerated 2^inputs cube corners over every declared
input, including single-grid-point (degenerate) dimensions that
contribute nothing. Interpolate over the effective dimensions only, so
a 15-channel CLUT of one-point dims no longer does 2^15 iterations per
pixel.
Findings:
- acfbe3a3-ee58-4b7e-b72a-5a4877dd15c9 (CWE-407, repeated flat-run scans)
- 9fcc72e7-46d6-4f75-9333-cb14a65eeafd (CWE-407, degenerate-dim corner blowup)
The decode admission counted each pixel's channel samples and vector
header but not the heap capacity of the per-pixel (id, coverage) pair
lists. A compressed Cryptomatte document could therefore return pixel
storage exceeding the caller's max_alloc budget.
Include the maximum pair capacity (one pair per rank, 8 bytes each) in
the per-pixel byte count before check_bytes.
Finding: 5482ed4a-bc95-43fc-84d1-d5bfb6373761 (CWE-400)
Two deep-EXR allocation gaps:
- flatten() allocated the flattened output and the full display-window
stage (nc f32 samples per pixel) without a limit check; the deep decode
only admitted the data window at four bytes per pixel, before the
channel layout was known. Pass Limits into flatten and re-check both
buffers at the real f32 layout size.
- decode_deep built a per-pixel usize block index before validating the
chunk's declared packed sizes. Compute the block bounds first, validate
the payload is present, then materialize the index, so a truncated or
lying chunk is rejected before the index is allocated.
Findings:
- feb6fcd8-58df-47e2-a04a-d4d81d5565aa (CWE-789, display-stage allocation)
- 9c53025d-65f8-4bcc-9161-143a4c5d7f46 (CWE-789, index before payload check)
When transforms (orientation/clean-aperture crop) are applied, admission
checked only the transformed output size. The delegated HEVC decoder
allocates the full coded image before cropping, so a HEIF with a small
visible aperture over a large coded image bypassed the limit policy.
Always check the coded dimensions against the full limits before
delegating, in addition to the transformed size.
Note: the related SPS-driven and grid-tile allocations (findings
75048229 and 2db4c59e) occur inside the pinned heic-rs dependency from
parameter sets the container does not expose; fully bounding those needs
a decoder-side change and is tracked separately.
Finding: ec5cec31-8a62-4963-8045-e3ec9d9e4931 (CWE-400)
Tiff::dump appended each entry's full ASCII value with no per-value or
total-output limit. Thousands of entries can reference one large aliased
ASCII value, expanding a small diagnostic input into a multi-gigabyte
String.
Cap each ASCII preview to 256 bytes (truncated at a char boundary) and
stop the listing once the accumulated output reaches 1 MiB, marking it
truncated.
Finding: d994ec7f-25dd-4ae7-9673-04adc0aa8a3c (CWE-400)
read_craw treats every segment as a run of full-width image rows and
grew the row-reference vector before checking coverage. A tiled layout
(which does not describe full-width rows) with repeated tile offsets
could push far more than height rows, allocating gigabytes before the
post-loop coverage check ran.
Reject tiled cRAW layouts up front (cRAW is only stored as strips) and
stop collecting rows once the image height is reached.
Finding: 3c2601a1-ad92-45b6-96f7-be5da87d97eb (CWE-400)
compressed_row positioned the bit cursor by replaying every preceding
byte within its page (skip % PAGE calls of get(8)). For narrow images
this replay runs per row, so a small file could consume disproportionate
CPU just setting up cursors.
Add Bits::new_at, which sets the page position and remaining-bit count
directly from the row offset in O(1), and use it in compressed_row. A
unit test proves it matches the old byte-by-byte replay.
Finding: 2a7d3d4b-90ab-4887-9dba-c3884e224990 (CWE-407)
GainMap::row materialized the entire horizontal gain grid (points_h
floats) once per applicable image row, even though each output pixel only
needs two neighboring points. File-controlled image height and horizontal
map size multiplied into processing and allocation churn far beyond the
input size.
Add GainMap::at_rv, a direct four-corner bilinear lookup, and use it per
output column in gain_row so no points_h-wide row is built per image row.
A test proves at_rv matches the former row() + at() path.
Finding: 7ebec8df-1a7a-4d33-915f-097f8e9be0e5 (CWE-400)
upright_preview decoded a raw file's embedded JPEG with default codec
options, discarding the caller's DevelopOptions.limits. A compressed
embedded preview could therefore allocate and process far more pixel
data than the caller authorized during the fallback and color-fit paths.
Pass the caller's limits into upright_preview and translate them into
the JPEG DecodeOptions, so both the Unsupported fallback and the
preview look-fit respect the same policy.
Finding: d17bc286-6765-41ce-bd41-579ee5b75cfa (CWE-409)
Each DHT definition for table id 0..3 calls Huffman::new, which
allocates and fills a ~48 KiB lookup pair. A stream repeating DHT
definitions (and DNG tiles referencing such a stream) could rebuild
tables thousands of times, multiplying header-processing cost and
memory churn while producing little output.
A real lossless JPEG uses a handful of Huffman tables; cap cumulative
table builds per stream at 64 and reject streams that exceed it.
Finding: 7a31b581-d1b7-4f9c-91e5-ce42b47db87f (CWE-400)
read_plane decodes every tile in parallel and retains all decoded tile
buffers in before assembly copies only the useful pixels. Each
tile is charged individually, but file-controlled padding and repeated
source ranges can make the cumulative retained samples far exceed both
the input and the useful image.
Sum the padded tile samples (two bytes each) and reject the plane before
decoding when that total exceeds max_alloc.
Finding: e66981b1-0d7a-4738-a64c-37b5e8bf28d1 (CWE-770)
PDN conversion copied a referenced name into an owned String for every
output layer, while the NRBF budget charged the shared string only once.
Repeated references to one large name could multiply a small document
into gigabytes of retained metadata.
Cap each layer name at 64 KiB and the aggregate copied name bytes at
4 MiB, checked before each copy.
Finding: fd77f4b2-39a2-48b5-b9d8-4ad69cf69469 (CWE-770)
The XCF importer reparses every layer offset and allocates a new owned
name and item path per reference. Repeated offsets referencing one
record with a large name or path could retain gigabytes of metadata,
which the per-field and per-layer limits did not bound in aggregate.
Add a document-wide metadata budget (32 MiB) to the Image reader and
charge each copied layer name and item path against it, erroring when
exhausted.
Finding: 1631d2df-fe8f-4734-b14e-380db11afb26 (CWE-789)
build_mask derives its working area from sparse mask geometry grown by
feather, so two distant changed pixels could span a canvas-sized
rectangle. It then allocated f32 planes (coverage, pixel-mask values,
blur scratch) over that area, gigabytes during even a one-pixel render.
Check the estimated working bytes before the dense allocation and fall
back to the constant default mask above 512 MiB, so pathological
geometry degrades feather detail instead of exhausting memory.
Finding: 8c04c5ef-a5bd-48f4-bfcf-83615bcf11c0 (CWE-770)
When a pattern exceeded the preparation cache budget, every lookup still
called Tile::new_in, expanding the whole sparse pattern into RGBA floats
on each output tile (and possibly concurrently). The cache budget only
controlled retained entries, not this conversion work.
Reject patterns whose full RGBA conversion would exceed an absolute
256 MiB cap on the uncached path, so oversized patterns are skipped
instead of re-expanded per tile. The cap is independent of the cache
budget, so small patterns still convert on demand when nothing is cached.
Finding: cc0b2f77-5578-48ed-aba0-1f3beccd21cd (CWE-400)
The version-6 slices parser matched each trailing descriptor ID against
the slice list with iter_mut().find(), an O(n*m) scan over two
file-controlled collections. A crafted resource with many slices and
many unmatched descriptor IDs could therefore cause quadratic CPU work
during import.
Build an ID-to-first-index map once and use it for descriptor lookups.
or_insert preserves the existing first-match behavior for duplicate IDs.
Finding: cf3f9ceb-d6df-4087-a2d6-dbe0f12657c3 (CWE-407)
Pin third-party actions to full-length commit SHAs (with version comments)
instead of mutable tags/branches to prevent supply-chain tampering via
retagged releases. Resolves SonarCloud rule githubactions:S7637.
The Fuzz step interpolated the user-controlled workflow_dispatch input
${{ inputs.fuzz_seconds }} directly into a run: shell command, allowing
script injection by whoever triggers the workflow. Pass it via an env var
(FUZZ_SECONDS) and reference "$FUZZ_SECONDS" so the value is treated as
data, not shell. Resolves SonarCloud githubactions:S7630 (BLOCKER).
Calls storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main
with secrets: inherit. Adds SonarCloud badge to README where missing and
removes the redundant sonar-project.properties (config now passed by the
reusable workflow).
* ci: refactor workflows to use reusable artcraft-common-actions
* ci: point reusable workflows to storytold/craft-common-actions
* ci: pin storytold/craft-common-actions to commit 27a978f and pass secrets explicitly
* ci: use @main (unpinned) for craft-common-actions with explanation comment
* fix(ui): respect native desktop scale under Auto UI scaling (#1278)
* UI scale: drop the now-unused monitor size, update docs and scorecard
Merge main; Auto no longer reads the monitor size, so remove the parameter and its
computation, simplify the test, and update ui-design.md and UI-217-9 to match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>