Commit Graph
5343 Commits
Author SHA1 Message Date
Dylan M. Taylor 143c3977c2 Merge pull request #8959 from dylanmtaylor/security/cms-psd-raw-vector-parsers
security: bisect table/stop lookups; charge ICC curves; bound PSD/RAW buffers
2026-10-23 15:20:40 -04:00
Dylan M. Taylor 5b93901779 Merge pull request #8956 from dylanmtaylor/security/ipp-reply-deadline
security(ipp): total reply deadline, resumable chunk walk
2026-10-23 15:20:37 -04:00
Dylan M. Taylor 7d4c7f3889 Merge pull request #8953 from dylanmtaylor/security/raster-doc-sparse-tiles
security(raster,doc): tile counts, shared rebuilt tiles, slice merge index
2026-10-23 15:20:33 -04:00
aiongg 437b012794 Merge pull request #8951 from aiongg/shared-gate
CI: the Linux gate runs through the suite's shared gate workflow
2026-10-23 15:08:49 -04:00
Dylan M. Taylor 75f5a9c126 packaging: use [[ for bash conditional tests (SonarCloud S7688) (#8914)
* 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 [[
2026-10-23 15:03:12 -04:00
Dylan M. Taylor 78ae50f7b2 security: bisect flat-table and gradient-stop lookups; charge ICC curve samples; bound PSD planes, RAW ljpeg retention and TIFF dump preview 2026-10-23 14:41:53 -04:00
Dylan M. Taylor 93e605a2e9 security(ipp): total reply deadline; resumable chunk walk 2026-10-23 14:41:49 -04:00
Dylan M. Taylor 3729c3f664 security(raster,doc): count tile grids without walking them; share rebuilt tiles; index slice merges
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.
2026-10-23 14:41:44 -04:00
Dylan M. Taylor 6da36b61f3 release: the git-sources gate moves into craft-common-actions (storytold/craft-common-actions#78) (#8952)
The check is shared at release.yml@main now; keep only the reusable call.
2026-10-23 14:41:09 -04:00
aiongg 9acf9a0cf9 CI: the Linux gate runs through the suite's shared gate workflow 2026-10-23 18:32:21 +00:00
Dylan M. Taylor b11ecc6ebf security(affinity): charge shared-object expansion against document-wide budgets (#8938)
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).
2026-10-23 14:30:21 -04:00
aiongg cf0960b520 Merge pull request #8915 from aiongg/ci-speed-2
CI: no debug info for dependencies, nextest in xtask ci, long sweeps spread over days
2026-10-23 13:48:36 -04:00
aiongg 9f0f7bc604 Merge pull request #8032 from aiongg/stored-path-resolve
Linked files: resolve stored paths with craft-docpath's resolve_absolute
2026-10-23 13:38:16 -04:00
aiongg 1f472d61ea Tests: report a bad PHOTOCRAFT_SWEEP_PART with assert!, which testkit's lints allow 2026-10-23 16:49:11 +00:00
aiongg 20ad9f1f4e Docs: the final check before a PR runs the full sweep 2026-10-23 16:40:24 +00:00
aiongg 066ad384cd Tests: refuse a PHOTOCRAFT_SWEEP_PART that isn't a part number 2026-10-23 16:40:24 +00:00
aiongg 7d9abd95af CI: the git-sources check parses .cargo/config and .cargo/config.toml
A source replacement, a patch or a path override in either cargo config never shows up in
Cargo.lock, so the check refuses them. Both files are parsed as TOML, as Cargo reads them.
2026-10-23 16:18:57 +00:00
Brandon ThomasandClaude Opus 5.5 e6d6fb45a4 Release 0.15.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 09:59:59 -04:00
Brandon ThomasandClaude Opus 5.5 ed1b6f4314 ci: run FreeBSD and Windows ARM64 checks on pushes to release
The release playbook runs the slow platform checks nightly and on every
push to release, so a regression shows up before a release is published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 09:59:59 -04:00
Dylan M. TaylorandCodebuff f27656b910 fix: restrict droplet shim permissions and annotate test range (SonarCloud) (#8870)
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>
2026-10-23 09:57:10 -04:00
Dylan M. TaylorandCodebuff f346b5c8f4 fix(vector): bound hostile geometry and raster work (#8769)
* 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>
2026-10-23 09:56:08 -04:00
92262d5950 fix(cms): confirm ICC profile identity by bytes, not the 64-bit hash alone (#8863)
Profile::content_hash is a fixed-key 64-bit SipHash of the ICC bytes, so a
crafted profile can share another profile's hash (about 2^32 work). Two
places took an equal hash as proof of an identical profile:

- transform::cached keyed its process-wide cache on the two hashes and the
  options, so a colliding profile was handed the other profile's cached
  transform and converted with the wrong colours.
- Profile::same_colors returned true on equal hashes, so a colliding
  profile was taken for the other one and a needed conversion was skipped.

Add Profile::same_bytes, which compares the ICC bytes (an Arc pointer check
first, then the hash, then the bytes). same_colors uses it instead of the
hash. Each transform cache entry now keeps the ICC bytes of its two
profiles; a hit counts only when they match, and a colliding pair gets its
own transform without replacing the cached entry. The transform is built
after the cache lock is released, as before.

The regression tests give Display P3 the content hash of sRGB (a stand-in
for a found collision): before the fix, same_colors called them the same
and the cache returned sRGB's transform for P3; now both see P3's colours.

Fixes #8613

Co-authored-by: John Turnbach <jturnbach@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 09:54:42 -04:00
aiongg ed1b9cff3f Merge pull request #8783 from wanikhawar/select-mask-edits
Select and Mask: tool edits as select.refineEdge params (engine, part 1 of 2)
2026-10-23 09:49:52 -04:00
Dylan M. TaylorandCodebuff 96042b8e7a fix(text): bound untrusted font and PSD text processing (#8768)
* 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>
2026-10-23 09:40:04 -04:00
Dylan M. TaylorandCodebuff 56d7009c78 fix(paint): validate gray tile dimensions safely (#8772)
* fix(paint): check tile dimensions before allocation math

Reject overflowing tile dimensions during deserialization and treat invalid in-memory tiles as empty safe previews and mipmaps.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* test(tile): assert the address-space overflow error on 32-bit targets

---------

Co-authored-by: Codebuff <noreply@codebuff.com>
2026-10-23 09:27:54 -04:00
Dylan M. TaylorandCodebuff ecff046ea1 fix(algo): avoid repeated copies of duplicate EXIF text (#8771)
* 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>
2026-10-23 09:27:48 -04:00
Dylan M. TaylorandCodebuff 1695f323e6 fix(psd): retain pattern budget charges after parse failures (#8767)
Keep allocation charges monotonic when malformed pattern blocks fail, so later sections cannot reuse budget already consumed.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>
2026-10-23 08:38:15 -04:00
Khawar Zamman WaniandClaude Opus 5.5 f58e2f4275 fix(algo,engine): refuse a Select and Mask refine too large to allocate
refine_mask_with builds its result densely over the selection's bounds
(grown by the spread, plus Refine Edge Brush additions). After a thin
edit across a huge canvas, or with a whole-canvas selection there, that
is billions of pixels. matting::refine_area names that area and
select.refineEdge refuses one over MAX_REFINE_AREA (2^30 px) with an
error before allocating, leaving the selection unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FszacAJDjznVimaxQktoJT
2026-10-23 14:41:20 +05:30
Khawar Zamman WaniandClaude Opus 5.5 56bbe6b80b fix(algo,engine): paint Select and Mask Brush, Lasso and Refine Edge Brush tile by tile
The Brush edit, the Lasso edit and EdgeBrush::new allocated a dense buffer
over their bounding box, which the swept-area cap does not bound: a 1 px
diagonal across a 100,000 px canvas sweeps ~141k px but asked for ~40 GB.

- matting::brush_surface (replaces brush_region) and EdgeBrush paint only
  the tiles a stroke touches (painted_tiles), capped at MAX_PAINTED_TILES.
- selection::polygon_surface rasterises only the tiles the Lasso's edges
  cross, byte-identical to selection::polygon; interior tiles share one.
- selection::combine_sparse adds/subtracts visiting only the new shape's
  tiles, keeping the rest of the selection shared.
- select.refineEdge refuses edits and refineStrokes that would cross too
  many tiles before painting anything, leaving the selection unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FszacAJDjznVimaxQktoJT
2026-10-23 14:22:33 +05:30
aiongg 18f4b5348c Tests: spread three format-by-geometry matrices over the days, and build one precheck state per case
The tilt-shift reference tests and tiled_clear_and_fill_match_one_pass_over_the_area cross pixel
formats or channel counts with independent geometry and fill settings. Each run takes a quarter of
the combinations through Sweep, a different quarter each UTC day.

precheck_agrees_with_execute built all five document states for every call and kept one; it now
builds only the state the call needs.
2026-10-23 08:30:48 +00:00
aiongg b87cfb3c6a Tests: spread the two longest sweeps over the days
swept_blocks_match_the_pixel_search skips the cases where the colour mode reaches extrude only
through a channel count and ink setting an earlier mode already covered. It and
damaged_layer_data_opens_flattened run a quarter of their cases per run, a different quarter each
UTC day (photocraft-testkit's Sweep). PHOTOCRAFT_FULL_SWEEP=1 runs every case, and
PHOTOCRAFT_SWEEP_PART=<n> replays the part a failing run printed.
2026-10-23 07:20:54 +00:00
Khawar Zamman WaniandClaude Opus 5.5 2d894eab2b fix(engine): tile-wise Invert edit; a run of Inverts costs one
Review of #8783:

- The Invert edit no longer builds a canvas-sized buffer: the canvas
  takes the inverse of the selection's untouched value (one shared tile
  for every fully covered tile, as fill_rect stores it), then only the
  tiles the selection holds are inverted. Byte-for-byte equal to the
  dense inversion; a 100k x 100k canvas inverts in well under a second.
- A run of Inverts is applied as one or none (pairs cancel exactly), so
  4096 of them cost at most one pass between other edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:42:26 +05:30
aiongg 175c9c1e75 xtask: test with nextest, doctests separately, one wasm check call
cargo xtask test and the ci test step run cargo nextest over libs, bins and tests when nextest is
installed (cargo test otherwise, with a notice), then cargo test --doc. The wasm check is one
cargo check with --keep-going over the wasm-safe crates, plus one call per optional web feature.
Clippy runs with --keep-going.
2026-10-23 06:00:38 +00:00
aiongg 6ed69874fd Profiles: no debug info for dependencies in dev and test builds
Workspace crates keep line tables, so panics in PhotoCraft code still show file and line.
2026-10-23 06:00:19 +00:00
Khawar Zamman WaniandClaude Opus 5.5 ca13e355c9 fix(algo,engine): bound brush stroke work and total brush edit area
Review of #8783:

- matting::brush `segments` no longer collects its pieces: they are made
  as they are painted, and each segment is first cut to the painted area
  grown by the brush. A 1 px stroke zigzagging across 2 million px
  (within MAX_SWEPT_AREA) split into ~134 million pieces collected up
  front (~2 GB) in brush_region and EdgeBrush::new; now ~5000 pieces.
- select.refineEdge `edits`: the Brush edits' swept area in all is
  bounded by MAX_SWEPT_AREA, as refineStrokes' is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:14:50 +05:30
aiongg a87b56b32a Merge upstream/main into stored-path-resolve 2026-10-23 05:08:54 +00:00
aiongg 82a7cfa3c5 CI: check the lockfile as Cargo reads it, and pin the release checkout
The git-sources step fails when Cargo.lock is missing or out of date with the manifests, when
.cargo/config.toml replaces or patches a source, or when the parsed lockfile has a git source outside
github.com/storytold. The release job's checkout is pinned to a commit.
2026-10-23 05:08:53 +00:00
aiongg 37463ae532 Stored paths: refuse Windows drive and share paths outside Windows without a document
Without a document, a relative path is reported as needing the document's folder. Outside Windows a
drive or share path such as C:\art\logo.png never becomes absolute, so it is refused as invalid
whether or not the document has a file.
2026-10-23 05:06:52 +00:00
Khawar Zamman WaniandClaude Opus 5.5 4970534e05 feat(engine): Select and Mask tool edits as select.refineEdge params
select.refineEdge takes `edits`: the Select and Mask workspace's Quick
Selection, Brush, Lasso, Object Selection, Select Subject, Clear Selection
and Invert, applied in order to the starting selection (or layer mask)
before the edge is refined. Cancel discards them, OK is one history step,
and agents drive them over MCP and the control channel.

- engine/select_mask_edits.rs: parse (bounded, every bad field an Err)
  and a pure apply/apply_one the workspace preview can share
- algo matting::brush_region: soft round brush coverage (hardness)
- ATN export reports steps with edits instead of dropping them
- "Edits" label in the 21 catalogs

Part of #7069 and #8148 (the workspace UI follows in a second PR).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:30:46 +05:30
Brandon ThomasandClaude Opus 5.5 ff47388101 Release 0.14.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.14.0
2026-10-22 20:06:08 -04:00
Dylan M. Taylor ee0d23f77a Merge pull request #8505 from dylanmtaylor/security/svg-svgz-double-decompress
security: stop double decompression of gzip-compressed SVG
2026-10-22 19:44:59 -04:00
Dylan M. Taylor ef4e13dc2d security(io): stop double decompression of gzip-compressed SVG
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)
2026-10-22 19:44:41 -04:00
Dylan M. Taylor 217af69bb5 Merge pull request #8504 from dylanmtaylor/security/svg-embedded-image-admission
security: admit embedded SVG raster images by header size
2026-10-22 19:42:44 -04:00
Dylan M. Taylor 6ed61e93a7 Merge pull request #8406 from dylanmtaylor/security/native-storage-safety
security: harden native temporary and recovery files
2026-10-22 19:42:39 -04:00
Dylan M. Taylor 3309b389a8 Merge pull request #8405 from dylanmtaylor/security/codec-allocation-bounds
security: preflight PCX and XPM allocations
2026-10-22 19:42:34 -04:00
Dylan M. Taylor 2bcf409ad3 fix(format): preserve user umask on atomic_write, 0600 only internal files, nonblocking sidecar/file reads 2026-10-22 19:35:51 -04:00
Dylan M. Taylor 1bd922f6d1 fix(codecs): use exact minimum encoded row length for PCX preflight 2026-10-22 19:35:37 -04:00
Dylan M. Taylor fcdb572cd6 security(io): admit embedded SVG raster images by header size
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)
2026-10-22 19:33:27 -04:00
Dylan M. Taylor 76f0e08df7 security: harden native temporary and recovery files 2026-10-22 19:28:44 -04:00
Dylan M. Taylor 827d949af9 security: preflight PCX and XPM output allocations 2026-10-22 19:28:08 -04:00