release.yml also runs nightly on main with craft-common-actions' dry-run: every release build
except macOS, with nothing signed, pushed or drafted. Breakage that only the release build hits
(riscv64, Flatpak, FreeBSD, packaging) then shows up the next morning instead of on release
day, as it did on 11 Nov 2026.
* Scripting: run-time compiled code passes the nesting check; bounded file masks, socket line search and file reads
eval and the Function constructors compiled script-built strings without
the bracket-nesting check the script itself passes, so a short script could
overflow the parser's stack. A host hook now checks every dynamic
compilation. Folder.getFiles turned masks into a backtracking regular
expression; a two-pointer wildcard match replaces it. Socket readln
rescanned the whole buffer after every read; only new bytes are searched.
Script files that are not regular files (pipes, devices) are not read.
* address review: check_nesting scans template interpolations, file masks keep wildcard length
* address review: file masks fold case as /i does; the nesting check skips regular expression literals
* address review: nesting scan reads ++/-- and keyword-named properties as division context
* address review: property-name state set at each word start; a number's decimal point is not a property dot
* address review: a leading hashbang line is a comment for the nesting scan; eval source is checked as it stands
* fix(scriptui): bound the edges and bands a fillPath is tessellated with
* address review: cap the triangles a ScriptUI fill produces
* address review: raise fillPath limits (65,536 edges, 262,144 bands, 4M triangles, work cap); fill what fits and log a warning instead of nothing
* address review: bound the fillPath crossing scan: edges swept by top, pairs and boundaries counted against the work and band budgets
* address review: bands with crossings that were not cut are left unfilled when the crossing scan is over budget
* address review: bands left out of a fill still count against the work budget
* fix(engine): index sequence runs and folder listings on import; probe relink candidates once
* address review: cap relink candidates before cloning paths; reuse a picked folder's listing for its run lookups
* Effects: cap Camera Lens Blur and median radii
Camera Lens Blur built its iris table (one row per pixel of radius) from an
uncapped Blur Radius before any size check, and the median window is filled
(2r + 1)^2 per row from an uncapped Radius, on the CPU and the GPU. The
radius is now capped where the work is built: 2000 buffer pixels for the
lens iris (UI max 500) and 512 for the median window (UI max 255), on both
paths.
* address review: cap camera lens reach with the spans
* track: face skin search visits only window ring pixels; cap stabilizer smoothing window
* address review: stabilizer window test asserts the cap with a far jump
* address review: smoothing radius covers every supported Smoothness (1000%, Smooth More, 240 fps); a frames x radius work cap bounds planning
* Camera tracking: repeated observations of one camera combine into one contribution per parameter
* Camera tracking: parameter-slot lookup for points that repeat a camera
* VP9 encoder and WebAssembly plug-ins: refuse short planes, limit memories and tables
encode_frame sized and filled superblock-aligned coding planes from the
configured dimensions (up to 65536 x 65536) even when the planes passed in
held a few bytes, making up the missing samples; it now encodes nothing for
planes shorter than the frame. The plug-in sandbox capped each linear
memory but not how many memories or tables a module declares, or their
elements; one memory, four tables and 65,536 elements per table are
allowed.
* address review: checked size arithmetic in the encoder's short-plane guard
* fix(flatpak): allow release checks and self-update helpers
* test: keep Flatpak permission parsing within strict Clippy rules
* chore: bump version to 0.24.1 for the Flatpak update fix
* Render settings: a saved Frame Rate override is held to the supported rates
* Render settings: describe shows the rate an out-of-range override renders at
* PDF functions and mesh shadings: charge retained values, refuse short tables, bound lattice rows and raster work
Depth-limit fallbacks were not charged to the function node budget; cached
copies of a function deep-copied its coefficient and program vectors while
only its nodes were charged; sampled functions made up missing samples as
zeros; calculator programs had no size limit; a lattice row was filled
before any limit applied; and overlapping mesh triangles each scanned their
whole bounding box.
The node is charged before the depth check; coefficients, bounds, encode
arrays and calculator operators are charged (eight to a node) so cached
copies carry their cost; a sampled function shorter than its table is
unsupported; calculator programs hold at most 4,096 operators; lattice rows
past the triangle limit are refused and vertices stop at it; and triangle
bounding boxes share a budget of 64 passes over the raster.
* address review: bound calculator token count while parsing
* address review: count skipped calculator tokens toward MAX_CALC_OPS
* address review: skip arrays and dictionaries in a calculator program without building them
* address review: scalar tokens in a skipped compound are passed over without building them
* Text: run lookup by bisection, bounded path flattening, font folders and files shared
run_at scanned the style runs from the first for every character; the
ordered runs are now bisected (the scan remains for gaps). Path text
flattened at a fixed 0.05 tolerance, so huge coordinates made hundreds of
millions of points; the tolerance grows with the path's size and
non-finite paths measure nothing. Font name records aliasing one string
are decoded once. The font folder walk listed folders reached through
several links again along every path; each real folder is listed once.
Faces of one font collection each read the whole file; they share it.
* address review: font walk breadth-first across roots, font file cache keyed by size and mtime and read outside the lock, first-subpath path measure with finite check, scratch dir guard
* address review: font files shared through file_bytes with the open file's metadata; run lookup by bisection only for ordered runs
* Pass the runs' ordered flag with them to stay under the argument limit
* address review: reversed run ranges take the scan
* Project: layer name suffixes in one pass; template names bounded and never a parent folder
unique_layer_name tried each numeric suffix against every layer, so
importing many layers of one name was cubic. The suffixes in use are now
gathered once. sanitize kept '.' and '..' (a comp name used as a path
component in an output template could point at the parent folder) and
copied a name of any length into every template token; dot-only names
become underscores and names are cut to 255 characters.
* address review: limit sanitized names to 255 bytes
* address review: keep long sanitized names distinct with a hash suffix
* address review: unique layer names use the numbered_name stem rule
* EPS: bound copies, storage, text outlines, group nesting and font discovery
The PostScript interpreter's numeric copy grew the operand stack past its
limit and charged nothing for the values copied; array and string
constructors retained storage without charging it; show and charpath copied
every glyph outline into the text path before any path or budget check; and
the font pre-scan searched the whole prefix again for every eexec, as the
bounding-box scan did for every rejected marker on a line. Clipping groups
also nested without limit.
Copies past the operand limit copy nothing; copies, arrays and strings count
against the operation budget; each outline is charged and checked against the
path limit before it is copied; headers are found in one pass and each is
tried once; a rejected bounding-box comment is skipped to its line end; and
the output builder drops groups past 256 open ones.
* address review: EPS font look-back stops at the previous font; charge input characters
* address review: count enclosing groups against the nesting limit in pattern and mask builders
* address review: close a replaced soft mask before nesting the new one; skip patterns whose wrappers pass the depth limit
* Tracking: bounded crop size, Roto chain span and resection copies
A stored pre-tracking blur or search area sized the padded crop with no
limit; both are held to what the frame can use (blur at most 100, as the
options command allows). A Roto Brush span from the project made one render
build a key for every frame of it, rescanning all strokes per frame; the
chain reaches at most 200,000 frames either side of the base and strokes
are grouped by frame once. The camera solver copied the whole
reconstruction for every unsolved frame, even frames seeing too few points
to resect; those are skipped first.
* address review: in_span uses the MAX_CHAIN-bounded span, forward key range cannot overflow
* Box blur: count repeated edge pixels instead of visiting them
The starting window of each horizontal and vertical box pass visited every
position of the radius, including any number outside the image that repeat
the edge pixel, so a radius from a project could make one pass take
arbitrarily long. The pixels inside are summed once and the repeated edge
pixels are added as a count times the edge value.
* address review: skip zero-count edge pixels in the box passes
* PSD: bounded zip errors, budgeted prediction row, row-bounded PackBits, channel checks before the result
A failed inflate was formatted with Debug, which prints the whole partial
output; only the status is kept. The 32-bit ZIP prediction scratch row was
allocated outside the decode budget; it is charged now. PackBits packets
were appended whole even past the row's end, growing the reserved plane;
they are cut at the row. The composite and layer decodes allocated the
document-sized result before checking that the channel data exists; the
compression and raw/RLE lengths are checked first.
* address review: check short ZIP composite data and the layer mask channel before allocating the result
* address review: drop the channel prechecks that rejected PSDs the lenient decode accepts; keep the allocation bounds
* SVG: bound tessellation, kept geometry, attributes, entity expansion and re-rendering
Circles and rounded rects flattened at a fixed 0.01 tolerance, so a huge
radius made unbounded curves; the tolerance now grows with the radius as
arcs already did. Every path a <use> copies and every gradient reference's
stops were parsed and kept without a document-wide limit; both now share a
budget of 8 million items. An element with thousands of attributes made
the XML parser's duplicate check quadratic, and repeated entity references
could expand attribute text to gigabytes below the node limit; a preflight
refuses more than 1,024 attributes per element and entity references that
expand past 64 MiB. Nested knockout and non-isolated groups re-rendered
their children several times per level; node draws are now held to 64 per
node of the tree.
* address review: entity_bytes takes the largest declaration of each name
* address review: drop tessellation-tolerance, attribute-count and entity-size refusals; keep retained and draw budgets, warn when the draw budget is hit
* Simulations: cap the steps a layer time can ask for and bound checkpoints as they are made
* address review: keep the requested sim state when trimming checkpoints
* address review: drop only the excess sim checkpoints when trimming
* Roto Brush, Object Matte: decode frozen mattes only at the layer's full-resolution grid
* address review: frozen matte size mismatch falls back to computed matte
* address review: frozen-size-mismatch test compares against a render with no cache
* Puppet: clamp an imported Expansion before padding the outline grid
* address review: non-finite expansion compares with zero expansion in test
* address review: trace over-budget puppet outlines at a coarser resolution; NaN expansion test
* interchange: end export walks on folder and precomp cycles, and index each clip's transitions on import
Export followed a folder's parent chain recursively before caching it, so a
project whose folders loop back to themselves overflowed the stack. The walk
is now iterative and a repeated folder ends it (the cycle's top becomes a
top-level bin, with a warning).
comp_has_audio recursed into nested comps with no visited set: a precomp
cycle never ended and a chain of comps that each use the next twice took
2^depth steps. It now walks with a stack and a visited set.
Import scanned every transition of a track for every clip on it, quadratic in
a long flat edit. Each track now indexes its transitions by clip once, and
the per-transition warnings are said once per track. Warning de-duplication
uses a set instead of a linear search.
* interchange: reuse a folder's cached bin on export
Exporter::bin returned None when the folder itself was already cached, so
every item after the first in a folder was exported at the project root.
Return the cached bin when the walk starts at a cached folder.
* automation: confine batch steps whose params are a reference, and sequence render folders holding outward links
A confined engine.batch checked each step's file parameters before the batch
ran, but a step whose whole params was a `$N` reference became an earlier
step's result only at run time, so a file path in that result (for example a
manifest from essential.templateInfo) reached file.saveAs unchecked. Such
steps are now refused while file access is confined.
An image-sequence render writes numbered frame names that the check of its
output template never resolved, and each frame is created through any link
already at its name. A render output whose folder holds a link leading
outside the write roots (or one that cannot be resolved) is now refused when
it is queued and again when the queue renders.
* address review: check numbered render folders for outward links
* address review: fail closed when the numbered-folder scan cannot list its parent; unix-only symlink test
* address review: render-folder link scan fails closed on unreadable folders, entries and file types
* UI: ScriptUI images decoded within a size limit; effect detection answers each comp once
ScriptUI decoded any image and converted it to full RGBA without a size
check; images are decoded through image's limits (4096 px per side and a
matching allocation cap). comp_has_effects walked a nested comp again for
every layer using it; each comp is now answered once.
* address review: effect detection by breadth-first search so depth cannot hide a comp
* Variable stroke: the first curve approximation's tolerance scales with the path's extent
* Variable stroke: cut only curves with huge extents, leaving small curves at the pixel tolerance
Adds step-security/harden-runner (audit, pinned to v2.22.4 like
craft-common-actions) as the first step of each job that did not have it,
so every workflow in the org's public repos reports its egress to
StepSecurity. Audit, not block: block-mode allow lists come from each
job's observed traffic.
A GPU project whose Render Queue job measured the CPU faster (#3335) rendered
every frame of a batch at once on the CPU, one per thread, with no memory cap:
on a 16-thread machine at about 1.4 GB a frame the app ran out of RAM and
exited without an error (0.23.1, Windows).
While a job renders on the CPU after a GPU frame measured its working memory,
CPU frames in flight now fit half the memory the system has free at that size
per frame, 1 to one per thread, settled once per job and logged. The probe's
timed CPU frames use the same cap. EFFECTCRAFT_CPU_INFLIGHT still overrides
it; Software Rendering projects, with no measurement, are unchanged. The host
reads the free memory through the new JobOptions::free_memory.
* Scripting: export commands a script runs directly respect Allow Scripts to Write Files
CompItem.saveFrameToPng and exportAsMotionGraphicsTemplate checked the
file-write preference only in their JavaScript wrappers. A script could call
comp.saveFrameAs or essential.exportTemplate itself (app.run, __exec) and the
native handlers wrote the file with the preference off.
Session::refuse_export and write_output_with now refuse when the running
command was issued by a script and the preference is off, so every command
that writes a path it is given is gated the same way. The wrappers check the
gate with a new side-effect-free __file("checkWrite") instead of writing an
empty file at the destination.
* address review: clear stale export test dir before use
* test: allow script writes in script_imports_footage_and_saves
load_lut took any source containing "LUT_" for inline .cube text, so a
path like /luts/LUT_Rec709.cube was parsed as text, failed, and the
effect passed the image through. A .cube can only parse with a size
header and data rows on separate lines, so a newline (after trimming,
as the path already is) is what tells text from a path.
popup_key lowered only ASCII letters, so an item starting with a capital
outside ASCII (French "Écran") kept it and never matched the same word in
lower case. After Effects stores popup items in its UI language, and
#3432 starts matching such names through popup_alias, keyed by
popup_key. Use Unicode lower case; ASCII keys are unchanged.
* .aep import: recognize French names for Levels' Channel and Clip popups
After Effects stores a popup's items in the UI's own language. A project saved
by a French After Effects writes "RVB", "Activé" and "Désactivé pour la
couleur 32 bpc" for Levels' Channel and Clip to Output Black / White.
EffectCraft's own popup options are English only, so these never matched by
name and the parameter fell back to its default, reported as "has no
counterpart".
Add popup_alias, a small table of confirmed After Effects labels in other
languages, used as a fallback in popup_mapping after the exact name match
fails. A synthetic test reproduces the three losses from a real file and
checks they are gone, with the same values as the English original.
* Cover Levels' French Red, Green and Blue popup items too
popup_alias only mapped "rvb" (RGB), so a French Levels with Red, Green or
Blue selected still lost that selection the same way. Add "rouge", "vert"
and "bleu" (from the same real pdnm list, confirmed against it); "Alpha" is
already the same word in both languages. A direct test on popup_alias covers
every item of the Channel popup it claims to know.
---------
Co-authored-by: sunkidda <sunkidda@users.noreply.github.com>