Skip to content

Merged changes ahead of v6.5.2.202603 - #219

Merged
fdesbiens merged 27 commits into
masterfrom
dev
Oct 2, 2026
Merged

fdesbiens merged 27 commits into
masterfrom
dev

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

No description provided.

fdesbiens and others added 27 commits September 15, 2026 19:04
The per-edit disclosure named the product and model, so every tool and every
model version appended another line rather than recognising the one already
there. Deduplication was by exact string, which a version bump defeats.
Fourteen files here carried more than one line.

Every file now carries exactly one line, fixed text naming no product:

    Portions of this file were generated with AI assistance.

written with the comment character that file already uses. Precise attribution
stays on the commit, where the Assisted-by trailer is dated and attached to the
diff it describes. A header line cannot keep that record honest, because the
code it names gets rewritten and the line stays.

Comment-only, 70 files: every removed line was a disclosure line, every added
line is the fixed text, and no file is left with zero or more than one.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
prepare_release.sh committed the version passes with a Co-authored-by trailer naming an
AI. That trailer asserts authorship an AI cannot hold: the human contributor signs the
ECA and is solely responsible for the contribution. The script has not yet cut a
release, so nothing has shipped with it.

The commits now carry their subject alone. A version pass is mechanical sed output, so
no agent produces it at run time; an agent that runs the script records its own
Assisted-by trailer on that run instead.

Ran the patched script against a scratch clone targeting 6.5.2.202603. Both commits come
out with an empty trailer block, and the version constants and 47 port version strings
update as before.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
* Enabled complete GUIX regression and coverage checks on dev

Linux and Studio View validation did not run for dev changes. GCC 14 exposed regression
build failures, unchecked API mappings were incomplete, and coverage omitted optional
configurations.

Enabled the full 18-configuration Linux matrix and Studio View on dev, preserved the
existing Windows and manual packaging workflows, and pinned the reviewed ThreadX
bootstrap and reusable workflow. Corrected profile definitions, unchecked API
declarations, and test fixtures. Added failure propagation, bounded setup, JUnit
retention, raw coverage unions, partial-run labeling, and measured line and branch
floors. Pages remains restricted to successful complete master runs.

CMake/Ninja and CTest passed 1,849/1,849 tests across all configurations. Merged
coverage reached 46,115/46,888 lines and 25,635/26,675 branches. Ten report tests and
checkout, dependency, CTest, and missing/empty coverage failure probes passed.
Actionlint, ShellCheck, and whitespace checks passed. Remaining coverage gaps and exact
profile measurements are documented.

Assisted-by: Codex (GPT-6) <noreply@openai.com>

* Fixed fresh ThreadX checkout validation

A new clone created without checkout has an empty working tree. The dependency
cleanliness check treated its absent files as local modifications and stopped CI
before configuration.

Distinguished new clones from existing checkouts, checking cleanliness before
switching revisions only when a working tree already exists.

Fresh dependency setup passed. Repeated setup preserved the library modification
time, and an intentionally modified checkout was rejected. ShellCheck passed.

Assisted-by: Codex (GPT-6) <noreply@openai.com>

* Staged Linux regression compilation within runner time limits

The hosted Linux runner timed out its 15-minute build step after completing the first
large configuration and most of the second. No CTest or merged report could run.

Staged the full 18-configuration build across the reusable workflow’s bounded steps: the
default profile during installation, five profiles during build, and the remaining 12
before the complete CTest run. Tests run concurrently while raw coverage collection
stays serial. Each stage retains its profile list, timing, and diagnostics; any build
failure still stops the job.

The complete local staged run passed 1,849/1,849 tests. Merged coverage matched the
serial run exactly: 46,115/46,888 lines and 25,635/26,675 branches. Ten report tests,
actionlint, ShellCheck, and whitespace checks passed.

Assisted-by: Codex (GPT-6) <noreply@openai.com>
…so (#191)

* Normalized the AI disclosure line in the remaining non-source files

The repository-wide pass covered source files only, so these kept the older
per-edit form of the disclosure comment, which names a product and a model
version. Every source file already carries the fixed line.

Replaced it with the fixed text, using the comment syntax each file already
uses.

Comment-only. 3 files, one line each. The repository now holds 73 files
carrying exactly one disclosure line and none carrying the old form.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Added a check that keeps the AI disclosure comment in its one accepted form

Nothing enforced the disclosure convention, so the drift it exists to prevent
returned twice: once when a port merged after the normalisation pass carrying
the older per-edit form, and once because that pass had covered source files
only, leaving build files and scripts untouched for months.

Added scripts/check_ai_disclosure.sh, which rejects the superseded per-edit
form, a doubled comment marker, more than one disclosure line in a file, and
any spelling of the line that is not exact. It runs from repo_checks.yml, a
workflow with no path filter, because a source-path filter is what hid the
build files the first time.

The check passes on this branch. Each of its four rules was confirmed to fail
on a tree with that defect reintroduced, and to pass once it was removed.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
* Fixed the unbounded pixelmap index in the binary resource theme loader

_gx_binres_pixelmap_table_load() indexes the pixelmap table with map_id, read verbatim
from the resource, while the table is sized from the theme header's pixelmap count.
Nothing relates the two, so a resource declaring an index beyond the table drove the
loop past its end, storing GX_NULL and then a pixelmap pointer there. The status of
_gx_binres_one_pixelmap_load() was examined only after those stores, so a failed load
also stored an unset pointer.

An index that does not fit the table is now rejected, and the status is checked before
any store. The font sizing and load divergence reported alongside this defect is not
addressed here: its write is closed by the arena bound added with the glyph range fix.

All ten binary resource regression tests pass. There is no regression test for this
defect yet. The function is static and only reachable through gx_binres_theme_load(),
which needs a malformed theme fixture with a pixelmap header at a known offset.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Initialised the pixelmap table locals and reworded the bound comments

The two comments added with the pixelmap index bound described how the loop used to
behave rather than what the code does now, and map_id and pixelmap were still
declared without initialisers although static analysis cannot see that
_gx_binres_one_pixelmap_load() sets both on every success path.

Both locals are now initialised (MISRA C:2012 Rule 9.1), the comments state the
invariant each check enforces, and the file carries the AI assistance line.

All 10 binres regression tests pass in default_build_coverage, with no warnings
from gx_binres_theme_load.c.

Assisted-by: Claude Code (Opus 5.5) <noreply@anthropic.com>
The theme loader sized its buffer for the theme it was asked for and allocated it even
when the resource holds no theme with that id, giving a buffer of zero bytes. The load
pass has no bound of its own, so for an id past the last theme it read beyond the
theme table and wrote a theme record and its tables into that buffer. The theme count
comes from the resource, so a theme file declaring fewer themes than the application
loads was enough, given an allocator that serves a request for zero bytes.

_gx_binres_theme_buffer_allocate() now refuses a theme id at or past the declared
theme count with GX_INVALID_VALUE, before anything is allocated. A negative id
converts to a count no resource can declare and is refused by the same test.

guix_binres_theme_id_bound_no_output loads through an allocator that serves zero bytes
and guards every allocation: it segfaults on dev and passes with the fix. 18/18 binres
tests; 734/735 in default_build_coverage, the one failure a timeout in
guix_single_line_text_input_24xrgb, which does not load resources.

Assisted-by: Claude Code (Opus 5.5) <noreply@anthropic.com>
* Fixed the glyph range underflow and out-of-bounds write in the binary resource loader

A font page header carries its glyph range as two values read verbatim from the
file, and every consumer turns them into a count as last - first + 1. Only the
upper end was ever validated, so a page declaring first_glyph above last_glyph
made that count underflow to a very large number. The sizing pass then multiplied
it by a glyph record size without an overflow check, the product wrapped to a
small value, and the buffer it reserved was far smaller than the load pass went
on to fill with glyph records taken from the file.

The buffer belongs to GUIX rather than to the caller, so an application had no
way to size it defensively.

Four changes, each closing a separate step of that chain:

  - Validate the range in _gx_binres_page_header_load(), which is the one point
    both the sizing pass and the load pass read a page header through, so a
    malformed range cannot reach either. This alone stops the reported chain.
  - Use the existing GX_UTILITY_MATH_UINT_MULT for the three reservation
    multiplies. Only the accumulation was checked before, and the normal,
    compressed and kerning formats each had an unchecked multiply, so a fix
    confined to one of them would have left the other two reachable.
  - Compute the load pass count in the same width as the sizing pass. The two
    disagreed, UINT against USHORT, which is the second half of the defect:
    space was reserved for one count and written for another.
  - Reserve glyph space through a single helper that checks the request against
    gx_binres_buffer_size and only advances gx_binres_buffer_index once the
    records are known to fit. The index used to be advanced by the full amount
    before any write, so aborting on a bad glyph magic number left it past the
    end of the arena and a later load continued from a corrupted offset.

The record size is deliberately the second argument to the checked multiply,
because that macro divides by that argument. A sizeof() is never zero; a glyph
count is exactly zero when first_glyph is last_glyph + 1, which would divide by
zero if the range check above were ever removed.

Both public entry points are covered without touching either: gx_binres_theme_load
and gx_binres_font_load share _gx_binres_font_buffer_size_get and
_gx_binres_one_font_load through gx_binres_loader.h.

Verified against the reported input on the ABI it was reported on, a 32-bit build
where sizeof(GX_GLYPH) is 12. A page with first_glyph 0xEAAAAAAB and last_glyph 0
gives the count 0x15555556 from the report; before this change the sizing pass
returned GX_SUCCESS having reserved 32 bytes, and now it returns GX_INVALID_FONT.
Well-formed fonts are unaffected, reserving the same 36 bytes for a single glyph
and 1164 for the range 0x20 to 0x7E as before. With the range check disabled on
purpose, the same input stops at the checked multiply with GX_MATH_OVERFLOW, so
the later layers are live rather than decorative.

Builds clean with -Wall -Wextra across the extended-Unicode, kerning and default
configurations, and the 1315 files of common/src still compile with no new
warnings.

* Added a regression test for the binary resource font glyph range

Drives _gx_binres_font_buffer_size_get() over a hand-built one-page font, which
is where the glyph range is first turned into a count. Both public entry points
reach it, gx_binres_theme_load() and gx_binres_font_load(), so covering it covers
both.

Four cases. An inverted range using the span from the report, and one inverted by
a single glyph so the boundary is covered rather than only an extreme; both have
to be rejected, because that is what stops the count underflowing and the
reservation multiply wrapping. Then a single glyph and the printable ASCII range,
which have to keep succeeding, with a check that the wider range still reserves
more space than the single glyph. A wrapped multiply shows up as a reservation
that is far too small rather than as a failure status, so comparing the two sizes
is what would catch it.

Reverting the fix makes both inverted ranges return GX_SUCCESS instead of
GX_INVALID_FONT, so the test measures what it claims to.

The font is built in the test rather than loaded from a resource file, because
the size pass reads only a font header and a page header, and the defect is in
the arithmetic over two fields of the latter. The range check is not conditional
on the extended Unicode configuration: an inverted range underflows the count
whatever the width of GX_CHAR_CODE, and only the subsequent multiply wrap needed
a wide one.

* Extended the binres glyph range tests to the load pass and every format

The reporter supplied regression inputs for the advisory: an inverted range in each
of the three glyph formats, a span wider than a USHORT can count, and a well-formed
control. The test on this branch drove only the size pass, over the normal format
alone, so the writers, the compressed and kerning formats, and the width the load
pass counts in were all unexercised.

The cases move into a shared header that three tests run, one per font
configuration. The configurations build disjoint demo sets and none compiles every
glyph format, so a single test cannot cover them all: the default build covers the
normal and compressed formats, and the extended Unicode and kerning builds each
cover what only they compile. Extended Unicode matters on its own, because a 32-bit
character code is what lets a page declare a span wide enough to wrap the
reservation multiply. Every case now drives _gx_binres_one_font_load() as well as
_gx_binres_font_buffer_size_get(), and a well-formed font is additionally loaded
into a buffer too small for its glyphs, which is the only way to reach the writers'
own bound while the range check above them is in place.

The span case asserts that a range wider than 0xFFFF is accepted, not rejected.
That is worth stating because the input was offered as one a fix should reject.
Such a span is well formed, and a character code wide enough to index it renders
it, so rejecting it would refuse a usable font; what was wrong was counting it in a
USHORT in one pass and a UINT in the other. The case pins both halves: the size
pass reserves for the whole span, and the load pass refuses a buffer that holds one
record, which it can only do if it counted the whole span too.

733 tests in the default build, 3 in extended Unicode, 4 with kerning, all passing
except five in the default build that fail on dev as well, each reporting a missing
golden file. Reverting the loader fix fails 7 cases in the default and extended
Unicode builds and 9 with kerning, so the tests measure what they claim.

The suite already builds -m32, so these run on the 32-bit ABI the issue was
reported on, where sizeof(GX_GLYPH) is 12.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Bounded the font page record against the load buffer

Change 4 of this branch bounded the three glyph writers against
gx_binres_buffer_size but left the page's own GX_FONT record taken from the
buffer unchecked, which is the one write in the load pass that could still pass
the end of the arena. It is not reachable today: both passes derive the page
count as a USHORT from the same font header and both follow gx_font_header_data_offset
the same way, so they agree on how many pages there are. It is reachable the
moment they stop agreeing, which is exactly the failure this branch exists to
make impossible.

The record is now reserved through the same helper as the glyphs. The helper
reserves records of a given size rather than glyphs specifically, so it is
renamed _gx_binres_buffer_reserve.

The test gains a filled margin after the buffer the loader is given, checked
after every load. A status on its own cannot show this: with a buffer too small
for the page record the glyph reservation fails too, so the load reports a
rejection whether or not the record was written first. The margin is what
distinguishes them. Removing the bound leaves the margin broken in two cases and
the tests red; restoring it leaves them green.

733 tests in the default build, 3 in extended Unicode, 4 with kerning, unchanged
from before this commit apart from the new assertions.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
* Bounded binary resource reads by the extent of the resource

The binary resource loaders received the resource as a bare pointer, and
GX_BINRES_DATA_INFO had no field to carry its length. Every read was therefore
performed at an offset and for a count the resource itself declared, against a
buffer whose extent the loader did not know. A resource declaring more than it
holds was read past its end, and a glyph's map offset became a pointer that was
kept in the loaded font and followed at draw time, so an attacker chose what
memory was read and rendered.

GX_BINRES_DATA_INFO now carries the extent, and _gx_binres_range_check() gates
every header the loaders read, the offsets a resource supplies for its font and
pixelmap data, the standalone resource offset table, and the three string table
walks. The header loaders already returned a status that most callers dropped;
those now propagate it, because a rejected header must not be left to be caught
by a magic number check over an uninitialised struct.

The extent reaches the loader through six new entry points that take it:
gx_binres_theme_load_ext, gx_binres_font_load_ext, gx_binres_pixelmap_load_ext,
gx_binres_language_count_get_ext, gx_binres_language_info_load_ext and
gx_binres_language_table_load_ext2, the last named so because _ext is already
the GX_STRING form. The existing entry points keep working and derive an extent
from the size the resource declares, which catches truncation but not a resource
built to mislead, since the same attacker chose that size. That split follows
the deprecated string API: GX_DISABLE_DEPRECATED_BINRES_API turns off the
pragma pointing at the _ext forms.

Two limits worth knowing. A resource declaring a data size of zero is treated as
having an unknown extent rather than a 20-byte one, so the older resources that
leave the field empty keep loading unbounded as they always have. And the
standalone format that gx_binres_font_load and gx_binres_pixelmap_load read
carries no total size at all, so those two derive nothing and only their _ext
forms are bounded.

Verified under AddressSanitizer on a 32-bit build. Through the _ext entry points
with a true length, a page declaring 101 glyph records in a 193-byte resource is
rejected with GX_INVALID_FORMAT where it previously read off the end, and so is
the span of 65537 from the advisory for this defect; well-formed resources still
load. Through the old entry points, the same input with a correctly declared
size is rejected and a valid one loads.

733 tests in the default build, 3 in extended Unicode, 4 with kerning, all
passing except five that fail on dev as well, each reporting a missing golden
file. Neutering the range check turns the four new cases red.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Deprecated the unbounded binary resource loaders and bounded the rest

The loaders that take only an address cannot bound a resource built to mislead, so a
replacement was added for each. Nothing removed the old ones, and the manual kept
pointing at them. Several reads reached by the new bound were also still unchecked:
a glyph's kerning table, a theme's color and palette tables, and every record the
theme loader placed in its buffer outside _gx_binres_buffer_reserve.

GX_DISABLE_DEPRECATED_BINRES_API now compiles the six old loaders out, following
GX_DISABLE_DEPRECATED_STRING_API, and disable_deprecated_binres_api_build covers that
configuration. Both binres tutorials use the _ext loaders; the STM32F439 path needs
the size of the image programmed to flash, so it gains a define for it. A glyph's
data block is bounded as a whole rather than by its map offset alone, which a Studio
font's alignment padding put outside the block; the kerning, color and palette tables
are bounded before being kept as pointers; the theme buffer is released on every
failure rather than one; and the pixelmap, theme, font table and pixelmap table
records go through the reserve, so a disagreement between the two passes ends in a
rejected resource. The char language table loader derives an extent like its
siblings, and re-checks the allocation pass has already made were removed rather than
left to imply they can fail.

guix_binres_ext_api_no_output drives all six _ext loaders and both layers: parameter
checks, a load at the true length, every length short of it, each byte corrupted in
turn, and an allocator that guards what it hands out. 137/137 binres, theme, font,
glyph and pixelmap tests; 729/734 in default_build_coverage, the 5 failures identical
on this branch before this change; 11 of the 12 binres files at 100% line coverage
and the theme loader at 98%.

One case in that test, the truncation sweep over a theme whose pixelmap data the
resource misreports, needs the fix on fix/binres-pixelmap-map-id and fails until that
branch merges. It passes with that branch applied.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Guarded the binres ext parameter checks against disabled error checking

The validation test asserts GX_PTR_ERROR, GX_INVALID_VALUE and GX_INVALID_SIZE
for null pointers, a negative theme id and zero sizes, and GX_SYSTEM_MEMORY_ERROR
for a missing allocator. Every one of those statuses is raised by the _gxe_
wrappers, which GX_DISABLE_ERROR_CHECKING compiles out. In
disable_error_check_build the arguments therefore reached the loaders, which
dereferenced a null root address and then the deliberately invalid pointer the
non-null checks are fed.

Those checks, and the invalid pointer itself, are now compiled only when error
checking is enabled. The allocator is still restored outside the guard, because
the rest of the test loads through it.

disable_error_check_build::guix_binres_ext_api_no_output segfaulted in
_gx_binres_resource_header_load before the change and passes after, in both
disable_error_check_build and default_build_coverage, with no new warnings.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The install step here does not only install: the regression suite is staged
across the install, build and test steps to keep each inside its limit, so this
one also builds default_build_coverage. The template's ten minutes is sized for
a consumer that installs packages and stops, which every other one does.

Measured on this job: the best install step on record took 8.2 of the ten
minutes, and four have been lost at the cap. That is a chronically marginal
budget rather than an unlucky one, so the packages and the ThreadX checkout only
have to run slow for the build to lose its remainder.

Repinned to the template revision that takes install_timeout_minutes, and asks
for twenty. Nothing else changes, and no other consumer is affected: the input
defaults to the ten they have now.

The build does not belong in this step at all, which is issue #205.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The regression job clones ThreadX to get its build bootstrap, under a 180
second budget, and that budget has run out twice on dev. The step cap then
absorbed the pinned-revision fetch that follows, so a run spent its whole ten
minutes on two network commands and never reached a test.

The clone now filters blobs. Only one pinned revision is ever checked out, so
the history's blobs are never wanted: the full pack is 26.86 MiB, blobless is
3.7 MiB to clone and 9.3 MiB once the revision's own blobs arrive on checkout.
Nothing else changes, and the revision pinning is untouched.

Measured end to end from an empty directory: clone 0.76 s, checkout 2.75 s, HEAD
matching the pinned revision and the bootstrap script in place. Run through
run.sh itself with the existing checkout removed, which produced the same.

This does not make a dead link work. It cuts what the budget has to cover by
roughly two thirds, which is what turned a slow mirror into a failed run.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
…207)

The build step builds five configurations, because the suite is staged across
the install, build and test steps. The best run on record took 11.8 of the
default fifteen minutes, which is 79 per cent of the cap, and six advisories
added regression tests this week, so the margin is closing rather than steady.

Repinned to the template revision that takes build_timeout_minutes, and asks for
twenty five. The install timeout set earlier is carried across unchanged.

No other consumer is affected: the input defaults to the fifteen they have, and
their build steps measure 0.5 minutes for LevelX, 1.1 for FileX and 6.4 for
USBX.

Moving that work out of these two steps is issue #205, which the measurements
now point at the test step for, since it uses 30.9 of its sixty minutes.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
* Fixed the JPEG marker ordering and segment validation defects

Covers both JPEG advisories, since one dispatcher carries all of it.

The marker loop was a stateless switch. Nothing stopped a second frame header,
and the scan case broke out of the switch rather than the loop, so scanning
continued afterwards and a later frame header re-entered the parser once the
output buffer had been allocated and its geometry latched. The pixelmap was then
published from a mixture of both generations of state.

Separately, each segment handler read a fixed number of bytes while only the
declared segment length had been bounded against the buffer, the scan header's
return status was discarded so its component-count check had no effect, and the
chroma sampling factors were never checked for zero, which left a decode loop
able not to terminate.

Five changes:

  - Track the frame header and the scan in the dispatcher. A second frame header
    is refused, as is one arriving after the scan, and a scan is refused when no
    frame header preceded it or when one has already been decoded.
  - Publish the pixelmap from the latched output geometry alone. The dimensions
    and the reported size came from different generations of state, so the
    structure described no allocation that had ever existed, and a consumer
    bounding its reads by that size was out of bounds as well.
  - Validate each segment's declared length against what the handler will read,
    before reading, in all four handlers. The quantization table case had the
    check already but ran it after the read it was guarding, and its length
    subtraction could underflow on a segment shorter than its own length field.
  - Capture the scan header's status, and validate the component count before
    storing it rather than after, so a rejected count neither survives in the
    decoder state nor gets ignored by the caller.
  - Reject zero sampling factors. The horizontal factor is bounded at the JPEG
    maximum of 4 rather than at 2, because 4:1:1 images carry 4 and decode
    correctly today; the vertical factor keeps its existing limit.

Verified against the JPEGs already in the repository and against malformed files
derived from one of them, so that only the property under test is malformed.

No regression: all 38 decodable JPEGs under test, tutorials and samples still
decode, and the four subsampling variants in guix_studio_test are byte for byte
unchanged at 330x199 with the reported size equal to the allocation. The two
files rejected for exceeding the resolution limit were rejected identically
before this change.

Malformed inputs, before and after: a zero horizontal and a zero vertical
sampling factor both ran without terminating and are now rejected; a short scan,
a short frame header and a short restart interval were accepted and are now
rejected; an over-range component count in the scan returned success and now
returns failure, which is the discarded status being acted on; a duplicate frame
header and a scan with no frame header were accepted and are now rejected.

Two limits of that verification, since they matter for what still needs checking.
A frame header appended after the scan is not reached by the marker loop in these
files, so the after-scan branch of the ordering check is not exercised by them,
and the published mismatch itself was therefore not reproduced here. Truncating a
segment inside a large file makes the handler read past the declared segment but
still inside the allocation, so those reads are not visible to a sanitizer; the
reporter's minimal files are what demonstrate them.

Builds clean with -Wall -Wextra, and all 1315 files of common/src compile with no
new warnings.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Added marker-ordering and segment-length cases to the JPEG decode validation test

The existing test already drives _gx_image_reader_jpeg_decode() over a table of
malformed files and requires each to be rejected, so these belong there.

Six files, 14 to 40 bytes, each isolating one defect: a DQT, a frame header, a
scan header and a restart interval whose declared length is shorter than the
fixed number of bytes the handler reads; two frame headers, since a JPEG carries
one and accepting a second let a file change the image dimensions after the
output buffer had been allocated; and a scan with no frame header before it.

The DQT case declares five bytes rather than the two its length field alone
occupies. A segment declaring only its own length is not malformed - it simply
carries no tables, and the handler correctly accepts it - so the length has to be
short rather than absent for the case to exercise the guard it claims to.

Reverting the fix makes the test fail.

Two parts of that fix are not covered here. The zero sampling factor needs a file
that reaches decompression, which needs valid quantization and Huffman tables
rather than a handful of bytes. The pixelmap publish consistency needs a file
that decodes successfully and then presents a second frame header, and the
malformed-input table only expects rejections. Both remain covered by the
verification recorded in the pull request.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Covered the remaining JPEG validation paths in the decode regression test

Three branches added by the decoder fix were never reached: a frame header too short
for its components, a scan header shorter than three bytes, and a scan too short for
its components. The short scan case carried no frame header, so the ordering check
rejected it before the length check it names. Nothing exercised a zero sampling
factor, a DQT length below two, or an over-range scan component count.

The short scan case now follows a valid frame, and six cases cover the rest.

Every branch the fix added is now reached, each once. With the fix reverted, the
zero-factor cases hang, and the short scan, both component-length cases and the
component count return GX_SUCCESS. The two DQT cases still return an error without
the fix and only show the over-read under a sanitizer. All 4 JPEG tests pass.

Assisted-by: Claude Code (Opus 5.5) <noreply@anthropic.com>

* Bounded the JPEG luma sampling product to the size of the luma block buffer

The sampling check accepted h up to 4 with v up to 2, but gx_jpeg_Y_block holds 256
bytes, four 8x8 luma blocks, and the MCU loop places the second block row at a fixed
offset of 128. With h=3 or h=4 and v=2, luma blocks were written up to offset 383 and
pixel output read up to offset 511, past the Y block and into the neighbouring
fields of GX_JPEG_INFO. Both stay within the structure, so the result is a wrongly
decoded image rather than a heap overrun, and both decoded with GX_SUCCESS.

The check now also rejects h * v > 4, which still admits 4:1:1 (h=4, v=1) and 4:2:0.
The comments added with the fix, in the decoder and the test, are reworded to state
what each check enforces.

Instrumenting the block writer, h=2 v=2, h=4 v=1 and h=1 v=2 stay within 256 bytes;
h=3 v=2 and h=4 v=2 reached 319 and 383. Three new cases (3x2, 4x2, 1x3) are rejected,
and gcov shows every branch of the sampling check taken. All 4 JPEG tests pass;
727/732 in the default suite, the same five failing without this branch.

Assisted-by: Claude Code (Opus 5.5) <noreply@anthropic.com>
* Fixed the duplicate IHDR heap out-of-bounds write in the PNG decoder

The chunk dispatch loop tracked whether IDAT had been seen, and whether a tRNS
had already supplied transparency, but nothing stopped IHDR being read a second
time. A PNG carries exactly one IHDR and it comes first, so a file that places
another one after IDAT rewrote the width, height, bit depth and pixel depth after
the decoded data buffer had already been allocated and filled from the original
values. The unfilter pass then derived its own extent from the new dimensions and
walked the old allocation, moving image bytes past the end of it.

The inflate path was never the problem: it bounds every write against the
allocation and even asserts that the decompressed length matches. Only the pass
after it worked from dimensions that could have changed underneath.

Two changes:

  - Reject a chunk order that a valid PNG cannot have. A second IHDR and a second
    PLTE are refused, and IDAT before any IHDR is refused, since that is where the
    dimensions the buffer is sized from come from. This is the root cause, and
    the flags mirror the idat_done flag already in the loop.
  - Have the unfilter pass confirm its walk fits the data that was actually
    decoded before it starts. The two are normally the same and the check costs
    nothing when they are, but it is what keeps any future divergence between the
    allocation and the current dimensions from becoming a write past the end
    rather than a rejected image.

The second check is written as a division so there is no product to overflow.

This also settles what the decoder hands back. A duplicate IHDR made
gx_pixelmap_width and gx_pixelmap_height describe the second header while
gx_pixelmap_data_size still described the first allocation, so the returned
pixelmap claimed more pixels than it owned. Rejecting the duplicate removes the
cause, and the unfilter check stops the inconsistent structure being reached even
if dimensions ever diverge again.

Verified with an independently built reproducer rather than the reporter's, on the
same shape they described: an 8x8 grayscale PNG followed by a second IHDR of
16x256, 94 bytes, the same size they reported. Before this change AddressSanitizer
reports a 16-byte memmove 0 bytes after the 72-byte allocation, in
_gx_image_reader_png_unfilter at the memmove, called from the decoder, on memory
allocated in _gx_image_reader_png_IDAT_chunk_read - the same three frames as the
report. After it, the file is rejected with GX_INVALID_FORMAT and no sanitizer
report. A duplicate IHDR before IDAT, an IDAT with no IHDR, and a re-entered PLTE
are rejected the same way. Well-formed single-IHDR and single-PLTE files still
decode to an identical 8x8 pixelmap with data_size 64.

With the chunk-ordering check disabled on purpose, the same input is stopped by
the unfilter check with no sanitizer report, so the second layer is live rather
than decorative.

Builds clean with -Wall -Wextra; the one warning the file still emits is a
pre-existing pointer-to-integer cast unrelated to this change, and all 1315 files
of common/src compile with no new warnings.

* Added chunk-ordering cases to the PNG decode validation test

The existing test already drives _gx_image_reader_png_decode() over a table of
malformed files and checks the status each one returns, so the chunk-ordering
cases belong there rather than in a new test.

Four files, each 44 to 129 bytes and built from a well-formed 8x8 grayscale base
so that only the ordering under test is wrong: a second IHDR after IDAT, which is
the reported defect; a second IHDR before IDAT, so the ordering state is tested
rather than only the after-IDAT case; an IDAT with no IHDR, where the buffer would
be sized from nothing; and a re-entered PLTE, which the loop also accepted.

All four are expected to return GX_INVALID_FORMAT.

The pixel data in the base image is zero on purpose. When a later chunk enlarges
the image, the unfilter pass reads its per-row filter byte at the wrong stride,
and zeros keep those misaligned reads valid, so the walk continues to the end of
the enlarged image instead of stopping early on an invalid filter type. With
non-zero pixel data the file is rejected before the defect is reached, and the
case would pass for the wrong reason.

Reverting the fix makes the test dump core rather than report a failed
assertion, since the first of these files then writes past its allocation.

Valid PNGs are already covered by the decode and draw tests elsewhere in this
suite, so a regression that rejected well-formed files would show up there.
* - fixed MISRA errors by removing semicolons from macro like defines.
- removed trailing white space characters.

* - main change is the semicolon added in line 2705 (as requested per review <#170 (review)>).
- all other changes are trailing whitespaces removed automatically by VSCode.
Every command in install.sh reaches the network and each already carries a
timeout, but none of them is retried, so one slow mirror minute ends the run.
That is what happened three times today: the package install is 41 MB across 34
packages and ran out of its six minutes partway down the list, twice while
fetching the 32-bit GCC runtime.

The three network commands now run under a retry loop with three attempts and a
widening pause, which is the shape ThreadX uses in tx_ci_common.sh and which
this script never picked up.

The worst case is unchanged: if all three attempts fail the run fails, as it did
with one attempt. What the loop buys is the common case, where the mirror is
slow once and not twice, and a log that names the attempt rather than a bare
exit 124.

This is worth having only because the step has room for it now. At the ten
minutes the install step used to have, a second full attempt could not have
fitted alongside the configuration build that also happens in this step.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The port stage matched a version only where "Version" was followed immediately by
four dotted numbers. A port writing three numbers, or a stray letter before the
first, was passed over and kept its old release, and the pass reported success
either way. Four ports across the suite had gone stale that way, one of them for
two consecutive releases.

The pattern now accepts three or four numbers and tolerates a leading letter. A
check follows it: any port header that names a release other than the one being
prepared is listed and the pass stops, so a port the substitution cannot reach
fails the release instead of shipping a version that misreports itself.

Verified both ways against a FileX tree: with a port whose string the
substitution cannot reach, the pass names the file and exits non-zero; with that
string corrected, all four ports update and the pass completes.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
#190)

Running git blame on a file header returns a tree-wide copyright, disclosure or
version-constant pass rather than the commit that last touched the code. There
have been 8 such passes in this repository since 2024, and every header line
in the tree now points at one of them.

Added .git-blame-ignore-revs listing those commits. GitHub applies the file to
its blame view automatically; locally it takes one git config command, which
the file documents in its own header.

Additive. No source file is touched, and every listed commit was verified to be
an ancestor of dev.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
* Linked GUIX readers to published documentation

The GUIX README and Studio guide commands opened archived Markdown pages.

I linked readers and Studio users to published HTML guides. The general documentation
entry now follows the latest release.

All replacement URLs returned HTTP 200; git diff --check passed.

Assisted-by: Codex (gpt-6-astra) <noreply@openai.com>

* Pointed the documentation links at the release they ship in

The links introduced here named 6.5.1, so the installer menu entries and the
Studio Help item would have sent 6.5.2 users to the previous release's
documentation from the day it shipped.

Four links across the README, the Studio Help handler and two installer menu
entries now name 6.5.2.

These are pinned deliberately rather than pointed at a floating path, so they
need revisiting at each release. That is the same handling the rest of the
version-bearing strings get.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
* Updated GUIX contribution guide for current workflows

The existing contributor guide omitted GUIX and GUIX Studio build, test, and
submission practices.

The guide now covers C99, all 18 Linux regression configurations, merged
coverage gates, Windows Studio validation, attribution, and release practices.

Local Markdown links resolve and git diff --check passed. Build tests were not
run for this documentation change.

Assisted-by: Codex (gpt-6-sol) <noreply@openai.com>

* Added visible spacing between contributor setup steps

The ECA and Git setup items had no visible gap in GitHub Markdown.

An indented line break now separates them while preserving list numbering.

GitHub Markdown rendered the items in one list with the visible gap, and
git diff --check passed.

Assisted-by: Codex (gpt-6-sol) <noreply@openai.com>
* Fixed MISRA rule 12.7 violation on GX_FIXED_VAL_... macros by replacing shift operations on signed operands with mul/div operations.
On these lines also rule 19.10 got addressed.

* Kept the arithmetic shift and made the radius signed at the call site

Replacing the shifts with division changes the result for every negative
operand: a right shift on a signed value rounds toward negative infinity and
division rounds toward zero. Measured over -5000 to 5000 the two agree on every
non-negative value and differ by one on every negative one, which is an
off-by-one pixel anywhere a fixed-point coordinate goes negative. The shifts are
restored.

The parentheses around the macro arguments are kept, because they fix a real
defect. In GX_FIXED_VAL_TO_INT the cast bound to the first token of its
argument, so at the one call site that passes a product the cast landed on the
radius rather than on the product. That is also why parenthesising alone fails
to build: the radius is UINT, the sine and cosine are not, so the product became
unsigned and -Wsign-conversion rejects it.

The cast that used to happen by accident is now written at the call site, where
it is visible and where the neighbouring branch already does it. The result is
identical to the code before this change.

MISRA 12.7 is deviated from here deliberately. The rule asks for unsigned
operands; making these unsigned gives a logical shift, which is the wrong
arithmetic for a signed fixed-point value, and changing the operator loses the
rounding.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

---------

Co-authored-by: Frédéric Desbiens <frederic.desbiens@eclipse-foundation.org>
…210)

repo_checks has been red on dev since the contribution guide landed. The guide
explains the disclosure convention, so it spells the accepted text and discusses
AI assistance in prose, and the near-miss rule flags any line mentioning AI
assistance that is not the fixed text character for character. Documenting the
rule was therefore a violation of it.

The guide is now excluded from the scan, for the same reason this script already
excludes itself: it has to spell the text in order to explain it.

The exclusion is by name rather than by extension, so a source file is still
scanned whatever it is called, and Markdown that is not the guide still is too.

Verified that the check still catches what it is for: a second disclosure line
in a source file and the superseded per-edit form both still fail it, and the
tree passes with neither present.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
… asset (#211)

The README linked the 6.5.1.202602a installer by its full asset URL, with the
version in both the tag and the file name. Nothing in the release process
updates it: the version pass covers the library and the port headers, and the
Studio script covers studiox.rc and the Inno script, so this line has to be
edited by hand or it advertises the previous release for the life of the new
one.

It now points at the releases page, which always carries the current installer
as an attachment. That removes the per-release edit rather than moving it
forward one version, and matches how the other repositories link to it.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
Two GUIX timer checks cast null arguments to CHAR **, which is incompatible
with the const-qualified ThreadX timer information API.

The calls now pass the untyped null macro, which is valid with both the earlier
and const-qualified ThreadX declarations.

Both changed sources passed C99 syntax builds with warnings treated as errors
against the updated ThreadX headers.

Assisted-by: Codex (gpt-6-astra) <noreply@openai.com>
Prepared by scripts/prepare_release.sh 6.5.2.202603, in the two commits it makes: the
version constants in common/inc/gx_api.h, then the version string every port reports at
run time.

48 files, 47 of them port headers. Every port now names one version and only one: a grep
across ports/ and guix_studio/ports/ returns a single distinct value, 6.5.2.202603. The
hardened pass from #200 reported no port left advertising another release, so GUIX does
not have the problem FileX had, where two ports had never been reached by any pass.

Build clean and 737/737 in default_build_coverage, and the AI disclosure check passes.

This is the library half. Studio carries its own version in studiox.rc and
guix_installer_release.iss, which scripts/update_studio_release_version.ps1 handles on
Windows; that is not part of this change.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
)

PowerShell 7.3 and later escape embedded quotes in native-command arguments as \".
ISCC does not understand that escape, so each /D version value it received from
generate_studio_installer.ps1 carried a stray backslash at both ends. The numeric
VersionInfoVersion directive rejected the result and aborted the compile; the other
three values were corrupted silently.

The /D arguments are now passed without embedded quotes. None of the values can
contain spaces, and ISCC still defines them as strings.

generate_studio_installer.ps1 -Version 6.5.2.202603 failed with ISCC exit code 2 on
PowerShell 7.6 before the change and produced
guix_studio_setup_version_6.5.2.202603.exe with it.

Assisted-by: Claude Code (Opus 5.5) <noreply@anthropic.com>
The library moved to 6.5.2.202603 in #212, but GUIX Studio still identified itself as
6.5.1.202602a in its resources, installer, MSIX manifest and project version number,
and the gx_api.h banner still read 6.5.1.

Prepared by scripts/update_studio_release_version.ps1 -Version 6.5.2.202603. Studio now
reports 6.5.2.202603, with 6.5.2.3 as the Win32 version, 6.5.2.03 for MSIX and
202603 as STUDIOX_VERSION_NUMBER.

No 6.5.1 remains in Studio's version-bearing files. The installer builds as
guix_studio_setup_version_6.5.2.202603.exe. Studio itself was not rebuilt from this
commit.

Assisted-by: Claude Code (Opus 5.5) <noreply@anthropic.com>
@fdesbiens fdesbiens changed the title Merged changes ahead of v.6.5.2.202603 Merged changes ahead of v6.5.2.202603 Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Test Results GUIX Studio Demo

227 tests  ±0   227 ✅ ±0   24m 3s ⏱️ + 4m 10s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 45e3fa7. ± Comparison against base commit 9664be3.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Test Results GUIX Studio Demo Compile

213 tests  ±0   213 ✅ ±0   19m 6s ⏱️ +3s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 45e3fa7. ± Comparison against base commit 9664be3.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Test Results GUIX

1 859 tests  +10   1 859 ✅ +10   2h 19m 27s ⏱️ + 1h 48m 19s
   19 suites + 1       0 💤 ± 0 
   19 files   + 1       0 ❌ ± 0 

Results for commit 45e3fa7. ± Comparison against base commit 9664be3.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Health
common.src 98% 96% ➖
Summary 98% (46403 / 47183) 96% (25881 / 26953) ➖

Minimum allowed line rate is 98%

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Test Results GUIX Studio View

40 tests  ±0   40 ✅ ±0   1h 22m 3s ⏱️ +4s
 1 suites ±0    0 💤 ±0 
 1 files   ±0    0 ❌ ±0 

Results for commit 45e3fa7. ± Comparison against base commit 9664be3.

♻️ This comment has been updated with latest results.

@fdesbiens
fdesbiens merged commit 9818c22 into master Oct 2, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants