Skip to content

Reject malformed hexadecimal in machine:readmem, machine:writemem and machine:debugreg - #888

Merged
chrisgleissner merged 3 commits into
GideonZ:masterfrom
chrisgleissner:fix/machine-api-hex-validation
Sep 11, 2026
Merged

chrisgleissner merged 3 commits into
GideonZ:masterfrom
chrisgleissner:fix/machine-api-hex-validation

Conversation

@chrisgleissner

@chrisgleissner chrisgleissner commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

Cherry-pick of #884 onto master. That PR was merged to test-merge
(bce4535) but belongs on master, so the same three commits are replayed here
with their original authorship. They applied without conflict, and all four
files are byte-identical to the merged test-merge result.

What it fixes

machine:readmem, machine:writemem (PUT and POST) and machine:debugreg
parsed their hexadecimal parameter with strtol(..., NULL, 16) and never
checked the end pointer. strtol skips leading whitespace, takes an optional
sign, and stops at the first character it cannot use, so input outside the
documented grammar parsed to something and was acted on:

The change

One parser for all four call sites, built on the chartohex helper already at
the top of route_machine.cc. It implements the documented grammar and nothing
else: hex digits, no larger than the caller's limit, no sign, no whitespace, no
0x prefix, no trailing characters. The limit is 0xFFFF for an address and
0xFF for the debug register, and it is checked inside the loop, so an
over-long value is refused as it is read rather than truncated.

machine:debugreg's API doc gains the 400 Invalid value it now returns, and
doc/api/rest_api_openapi_u64.yaml is regenerated to match. openapi_check is
a build gate, so the committed document cannot drift from the source.

The one form that used to be accepted and now is not is the 0x prefix. No
client in this repository sends it: the test harness formats %04X, and the
firmware's own web UI uses toString(16).padStart(4, "0") and literal
four-digit strings, with its typed-address path validating as a 16-bit hex
number first.

Tests

Two stages in tests/e2e/api/readmem_writemem_test.py, each gated through the
FIXES table so a machine whose released firmware predates the fix skips with a
named reason:

  • bad-address covers the six address forms on all three endpoints.
  • bad-debugreg covers the five value forms. machine:debugreg has a GET as
    well as a PUT, so "the refused request wrote nothing" is asserted directly:
    the register is read before and after each rejected write and has to be
    unchanged. It skips where the route does not exist, both debugreg routes
    being inside #if U64.

Verification on this branch

Built from master and JTAG-deployed to an Ultimate 64 Elite:

readmem_writemem_test: OK (78 checks, 23.3s)   --test all, all eight stages

and on an Ultimate II+L in a C64 Ultimate, flashed from this branch's CI
artifact (git_commit_hash 27cfe033):

readmem_writemem_test: OK (38 checks, 16.9s)   --test all

bad-debugreg skips on the cartridge, both debugreg routes being inside
#if U64. freezer-audio, which master gained in #886, also passes on both
machines against this build (u64 6/6 in 7.7s, u2@c64u 6/6 in 9.4s), so the
change sits cleanly on top of it.

Offline: lint_test, registry_test, openapi_contract_test,
check_transport_usage, stale_gates_test, make openapi_check and
make openapi_test (191 tests) all pass.

The red measurements above were taken on #884 by reverting each part of the
firmware in turn and redeploying. The code here is byte-identical to what was
verified there.

JC-000 and others added 3 commits September 11, 2026 12:07
strtol() yields 0 for an unparseable address, and the end pointer was
passed as NULL and never checked, so a non-hex address passed the
0..65535 range check and the request was served against $0000. For
writemem that is a destructive write to zero page answered with HTTP
200: the response even names "0000-..." as the address it wrote.

Check the end pointer at the three handlers that parse an address --
PUT and POST machine:writemem, and GET machine:readmem -- rejecting
input that consumed no digits or left trailing characters, folded into
the existing range check so the error path is unchanged. 400 "Invalid
address" is already the documented response at all three, so the API
documentation is unaffected. An optional 0x prefix still parses;
overflow needs no errno check because strtol's LONG_MAX/LONG_MIN are
outside the accepted range.

Cover it with a bad-address stage in the readmem/writemem E2E suite,
gated through the FIXES table so machines whose released firmware
predates the change skip with a named reason rather than going red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
strtol skips whitespace and takes a sign, so "-0", "+1" and " 1234" still
reached an address. parse_address accepts hex digits only.
The same parser, with a byte limit. "ZZ" wrote 00 and "1FF" wrote FF, both
answered HTTP 200.
@chrisgleissner
chrisgleissner merged commit c0ad8db into GideonZ:master Sep 11, 2026
1 check passed
@chrisgleissner
chrisgleissner deleted the fix/machine-api-hex-validation branch October 7, 2026 07:13
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