Skip to content

Decode a base64 run only where the scanner decodes it - #36

Merged
azrollin merged 1 commit into
mainfrom
t10/decoder-pi007
Oct 13, 2026
Merged

azrollin merged 1 commit into
mainfrom
t10/decoder-pi007

Conversation

@azrollin

@azrollin azrollin commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Closes the inherited pair of GLS-PI-007 rows named in #35 (one padded, one not) where the scanner blocks and the Worker allows. The same pair differs on main before #35. The rule set is untouched; this aligns which base64-looking runs the Worker's preprocessor accepts for decoding (padding acceptance) with the scanner. Two decoding differences remain and are listed under Limitations: invalid-byte skipping and the printable-character table.

Cause

The scanner reads a run of 20 or more base64 characters with Python's b64decode, which refuses a run whose padding cannot complete the last group and then leaves the run as written. The Worker used atob(), which is more forgiving: it accepts a run with no padding and refuses surplus padding. In the failing rows the text is written backwards and contains a 23 character run of letters and a slash. The scanner leaves that run alone and still reads the reversed phrase. The Worker decoded it to noise, which destroyed the phrase.

The same difference ran the other way: a run with surplus padding that the scanner decodes was left as written here.

Change

src/preprocessor.js, decodeBase64Segments only:

  • A run is decoded only where b64decode accepts it: one leftover character in the last group never decodes, two need both =, three need at least one, and padding beyond that is ignored.
  • The result must be longer than four code points (it was four UTF-16 units).
  • Invalid bytes are dropped, a leading BOM is kept (so the result is not printable), and a valid U+FFFD is kept.
  • The printable test follows str.isprintable() through Unicode categories (other, separator) instead of a short list of control characters.

The matching entry in LIMITATIONS and the README line are updated to what remains.

Test, red on main and green on this head

tests/base64_segment_padding.test.mjs with tests/fixtures/scan_corpus_base64_padding.json. Expected values come from the scanner's own function and scan (0.6.8, commit 0a3b4686). It covers both padded and unpadded forms of the failing rows (including 2,100 characters of padding that the normalizer removes, in two kinds), runs with each amount of padding, and the result checks above.

node --test tests/base64_segment_padding.test.mjs
main (8f5b870):  pass 2, fail 6
this head:       pass 8, fail 0

Differential against the scanner

decode_base64_segments on 546 generated texts (14 payload shapes x padding states, 300 random byte strings, raw alphabet runs of 20 to 27 characters), output text and recorded segments compared: 105 differences on main, 0 on this head.

Decision and sorted finding ids, one scan per row, stock rules:

set rows differing on main differing now
length rows from the #35 review 1,712 2 0
supplementary-padding rows from the #35 review 908 4 (2 decision, 2 finding-id only) 0
scanner-recorded corpus (scan_corpus_068.json) 1,307 1 1 (unchanged)
generated cue x payload set 12,614 16 16 (unchanged)

What remains different

  • The printable test reads this runtime's Unicode tables where Python reads its own, so a character assigned in one and not the other can decide a segment differently.
  • A valid U+FFFD is dropped along with invalid bytes when one segment holds both.
  • Offsets and windows count UTF-16 units where the scanner counts code points (the list in Port the 0.6.8 engine reads: folded regex view, negation walk, record hook #35). Tracked separately.

This PR makes no claim that the hosted demo matches the scanner beyond the rows and checks listed here.

Gates on this head

Private checkout of the scanner at 0a3b4686. All exit 0 on head 48409df: disclosure gate; policy parity (14 cases, 0 mismatches); engine parity (137 cases, 0 verdict, 0 finding-set deltas); channel parity (2,488 cases, 0, 0); wide parity (1,555 pairs, 0, 0, no known entries); counterexample parity (0 deltas, 0 slower than the bound); parity_test.py (882 regexes, 0 misses; 13,024 benign checks, 0 disagreements; seed 4173371, replay with PARITY_SEED); v-flag disclosure; metadata contract. node --test tests/*.test.mjs: 164 passed, 0 failed (156 on main, plus the 8 new). python3 -m pytest on the dotless-i tests: 62 passed. CI: wide-parity and workerd-runtime green.

A run whose padding cannot complete its last group is left as written, surplus
padding is ignored, the result is judged by code points, and the printable check
follows the scanner. A reversed text with a letters-and-slash run no longer loses
its phrase to a decode the scanner never makes.
@azrollin
azrollin merged commit 538cef9 into main Oct 13, 2026
2 checks passed
@azrollin
azrollin deleted the t10/decoder-pi007 branch October 13, 2026 23:07
azrollin added a commit that referenced this pull request Oct 14, 2026
…38)

When a base64 segment holds invalid bytes, only those bytes are dropped and a
replacement character that was encoded in the text (the bytes EF BF BD) stays
where it was written, as the scanner's decode with errors="ignore" does. The
strict decode is still tried first; when it fails, the bytes are walked against
the well-formed UTF-8 table and whatever is not well-formed is skipped, so the
work stays linear in the segment length.

This closes the 256 rows the review of #36 left open: texts the scanner blocks
that this runtime allowed because the encoded replacement character was deleted
along with the invalid bytes. The new decode agrees with the scanner on 60,001
byte strings, and on the 4,472 independent rows of that review this head
changes exactly those 256 rows and lowers none. One difference remains in this
entry and is still listed: the printable test reads this runtime's Unicode
tables where the scanner reads Python's, so a character assigned in one and not
the other can decide a segment differently. The rule set is untouched.
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.

1 participant