NRF LDN parser bug - found, fixed, verified¶
Date: 2026-08-11
Scope: libnrf (pcc-nd500), the NRF/DOM/DESC browser viewer, NRF-FILE-FORMAT.md
What was broken¶
nrf_read_group() in src/lib/nrf/nrf_utils.c of the pcc-nd500 repository, and the JS port of it
(nrfReadGroup() in viewer/index.html), never special-cased LDN (control number 27).
Every other NRF control group's numeric field (NL bytes, 0-7) is its entire payload. LDN is
the one exception: its numeric field is a byte count N (unsigned), and N more raw literal
bytes follow the header that are not a second numeric field and not a symbolic field. Both
parsers treated LDN like a plain group - consumed only the count byte, left N bytes of real
file content sitting in the stream to be misread as the start of the next control group.
Everything after the first LDN in the file desyncs from there: garbage symbol names, spurious
extra BEG/END pairs from noise, and eventually a "malformed group (numeric/symbolic field ran
past end of file)" near EOF once the drift consumes a byte count large enough to run past the
buffer.
NRF-FILE-FORMAT.md's own LDN row said "same as LDI but explicitly no S" - a guess by analogy
to LDI, never checked against real bytes. It was wrong: LDI's numeric field bytes are the
immediate data (capped at 7). LDN's numeric field is a count; the data comes after it,
unbounded.
How it was found¶
User reported NC-LIB-A06.NRF (101,442 bytes, from the ND-500 C library set) failing in the viewer with
"Group stream ended with a malformed group... 6098 control groups decoded, 180 module(s)
found." Traced with a standalone harness linked against the real nrf_utils.o (not a
reimplementation) that logs every group's offset/control/NL/symbol:
- Raw iterator hit the malformed condition at group #6098, offset
0x18c1e, ctrl=25 (MSG), NL=5 - a MSG group with a claimed 206-byte symbol that would run 182 bytes past a 101KB file. Obviously not real data; the parser was already desynced by then. - Walking backward through the trace found the actual desync point: group
#005961, offset0x01818a,LDN NL=1 num=52. The next 52 bytes (offset0x01818c..0x0181c0) were being read as the start of a new group ("LIB sym=67:'JHF IA'" - garbage), when they're actually LDN's own immediate-load payload. - Confirmed by hex dump: skipping exactly 52 bytes after the LDN header lands on
DEF #.646at offset0x181c0- a symbol (#.646) already seen live and legitimate earlier in the same module (module 177,DDF #.646).
The fix¶
Both nrf_read_group() (C) and nrfReadGroup() (JS) now do, after the existing
control/numeric/symbolic field handling:
if (cn == NRF_LDN) {
uint64_t skip = 0;
for (int j = 0; j < nl; j++) skip = (skip << 8) | group->numeric_raw[j]; /* unsigned! */
if (p + skip > it->len) return -1;
p += skip;
}
Note it recomputes the count from the raw bytes rather than using the already-computed
numeric_value / numericValue - that field is sign-extended two's-complement (correct for
every other control number, where the numeric field is a signed value), which is wrong for a
byte count (an LDN with NL=1 and top bit set, e.g. 0x80, must mean 128, not -128).
Verification performed (all actually run, not inferred)¶
make -C src/lib/nrf test- existing unit tests (nd-500-apf-lib-e.nrf, 5716 groups, 53 modules) still pass unchanged.- Rebuilt
nd500-dump/libnrf.afor real (not the/tmpexperimental copy) and ran-Nagainst four files:NC-LIB-A06.NRF(the reported file, 8354 groups, 103 clean BEG/END-paired modules, was 180 with garbage),CAT-LIB-B06.NRF(21824 groups),USLIB3.NRF(74857 groups), andnd-500-apf-lib-e.nrf(5716 groups, regression check). All four now parse to clean EOF withTruncated (malformed tail): no. - Extracted the literal
<script>contents fromviewer/index.html(not a port/rewrite), ran it under Node againstNC-LIB-A06.NRF: 8354 groups, all 101,442 bytes consumed,truncated: false- matches the C reference exactly. node --checkon the extracted script after editing (syntax only, not a browser render - no browser-automation tool was available this session; the user opened the real page and confirmed the failure, then I re-served the fixed page on port 8888 for them to re-check).
Files changed¶
pcc-nd500repository,src/lib/nrf/nrf_utils.c- the real fixSINTRAN/File-Formats/viewer/index.html- same fix, JSSINTRAN/File-Formats/NRF-FILE-FORMAT.md- corrected LDN rowSINTRAN/File-Formats/viewer/run.bat(new) - double-click launcher,cds toFile-Formats/and serves on port 8888 (not 8000 - changed at user's request, a local conflict)SINTRAN/File-Formats/viewer/README.md- documentsrun.bat, port 8888
Commit status: the pcc-nd500 fix was committed 2026-08-17 as c82cbfc, after an
independent re-verification (control run without the fix: all three libraries end in an
allocation failure with unclosed modules; with it, all three reach clean EOF and libnrf's
own tests still pass at 53 modules). The NDInsight side is committed alongside this
document.
Also this session: DESC format fields resolved (separate from the above)¶
A different session/LLM carved MON-DEBUG:PROG (the ND-500 Loader/Debug Monitor) and pinned
byte offsets for the Segment Entry fields that DESCRIPTION-FILE-FORMAT.md had previously
marked "UNABLE TO DETERMINE" - PLB, PSIZE, DLB, DSIZE, DEBUGINFO, DLINKDATE, ABSFIXAD,
LOWLOGFIX, PLOLOGFIX, PUPLOGFIX (all offset 88-129, see the doc section 4 for the exact
monitor instruction/label evidence per field). I:
- Updated the viewer to decode and display those ten fields for Segment Entries (previously intentionally hidden as unverified - see the viewer's own prior disclaimer, now stale and removed).
- While verifying the viewer's numbers against the real
description-file.desc(the vendor floppy210319H02-XX-01D), found the doc's own section 5 table had a transcription error:LINKAGE-LOAD-H02's DSIZE-stored value was listed as 2,109,654, but the real bytes at file offset0x4124(raw00 20 2e d6) are 2,109,142. This value fed directly into section 6, which called it an "open anomaly" (DSIZE not matching the.dsegfile size). With the correct value the anomaly resolves cleanly:DLB + (DSIZE_stored + 1) = 75834 + 2,109,143 = 2,184,977, exactly the.dsegfile size - the same formula the zero-DLB entries already satisfied trivially. Corrected both.mdand.json; moved that item fromopenQuestionsto a newresolvedQuestionsarray.
At the time, one thing was left open in DESC: the manual-vs-monitor conflict at Segment Entry
bytes 74-84 (manual says COMSEGSIZE/N100SEGNO arrays; the monitor code seemed to print two
byte strings there using a count at word 37B). Settled 2026-08-17: the manual was right -
the count is COMSEGNO and the "strings" were a misread loop. See DESCRIPTION-FILE-FORMAT.md
section 4.