XMSG field inventory — every field, its status, and what would settle it¶
Date: 2026-07-31 Purpose: the driver for "carve and test until all fields in all protocols are decoded and understood". One row per field. A field is only CARVED when the kernel's own source or symbol table says what it is — matching the wire is confirmation, never a substitute.
Status vocabulary, used strictly:
| Status | Means |
|---|---|
| CARVED | The kernel symbol table or source states it. Cite the symbol. |
| WIRE | Reproduced on captured traffic, but no kernel evidence for the meaning. |
| INFERRED | A reading that fits, with no independent support. |
| UNKNOWN | Not understood. Say so. |
1. What this session settled¶
The transported header layout is now CARVED from
SINTRAN/NPL-SOURCE/SYMBOLS/L07/XMSG-SYMBOL-LIST.SYMB.TXT and independently confirmed on
the wire. This resolves the conflict left open in
XMSG-SUBHEADER-NAMED-FROM-SYMBOLS-2026-07-29.md section 4.
| Symbol | Octal | Word | Wire offset |
|---|---|---|---|
XMTHD |
134 | 0 | 14-15 |
XMSTA |
135 | 1 | 16-17 |
XMDSY |
136 | 2 | 18-19 |
XMDPT |
137 | 3 | 20-21 |
XMSSY |
140 | 4 | 22-23 |
XMSPT |
141 | 5 | 24-25 |
XMCSM |
142 | 6 | 26-27 — header ends here |
XM5HE=7 (words) and XM5HL=16 octal (14 bytes) fix the length. XMLEN=147 is five words
past XMCSM and is therefore not a transported-header field at all.
Three consequences, each confirmed against all 1449 captured data frames:
XMCSMis ONE word. Our 32-bit reading at 26-29 straddles into the body.Flags2(wire 10-11) is a COPY ofXMCSM— equal on 1449/1449. The long-standing rule "Flags2 == XMCSM >> 16" was that equality seen through the wrong split.- The body starts at wire offset 28 = 13 (SINTRAN header) + 1 (Counter) + 14 (transported
header). Confirmed arithmetically: on the frames where
XMCSMis a length it equals the body length exactly (offset 0 across 492 frames) once 28 is used.
2. LAPB layer¶
| Field | Status | Note |
|---|---|---|
| Address | WIRE | 0x01 link mgmt, 0x09 even info, 0x89 odd info. Bit 0x80 = odd length. 0x07 seen on some ACK I-frames — UNKNOWN, tolerated. |
| Control | WIRE | mod-8 I/S/U. S-subtype via control & 0x0F. |
| FCS | WIRE | CRC-16 0x8408, residue 0xF0B8. Verified on every frame parsed. |
3. SINTRAN header - it is SEVEN WORDS, not "13 bytes + a counter byte" [2026-07-31]¶
The ND-100 is word-addressed and the kernel manipulates this header with word-indexed
instructions (LDA ,X 1 / BSET / STA ,X 1 in XSDGM at 137603), so the byte-oriented
description we have been carrying is a mis-framing. The header is 7 words, exactly like
the transported header that follows it:
21 13 | 00 0e | 00 64 | 00 66 | 03 d3 | 00 80 | d9 c1
w0 w1 w2 w3 w4 w5 w6
markers subtype dest src Flags1 Flags2 ProtoID:Counter
w6 packs the Protocol ID in its HIGH byte and the Counter in its LOW byte. The Counter
does not "sit between the SINTRAN header and the transported header" as previously recorded -
it is the low half of the last header word. 7 words + 7 words = 14 words = 28 bytes, which is
exactly the body start already confirmed independently from XMCSM == bodyLen.
This unifies the ACK model. An ACK frame reads
21 13 | 00 03 | 0066 | 0064 | 03d3 | 0001 | da 4b. What XMSG-PROTOCOL.md calls the ACK's
"single trailing payload byte" (0x4B here) is not payload - it is the low half of w6,
the same field as a Data frame's Counter, with the Protocol ID above it. Data frames and ACKs
share one header shape; only the subtype and what follows differ.
Consequence for the open offset-12 question: the Protocol ID is not written on its own. It is the high byte of a word whose low byte is the Counter, so whatever computes one computes both - which fits the envelope model, where Counter and channel fall out of the same seed relation. Look for code writing a COMBINED word, not a byte store to offset 12.
Corroboration from XSACK (140076): the routine opens with a run of BSET on T -
ZRO 20, ONE 10, ONE 0, ZRO 30, ZRO 50, ZRO 60 (remember the printed field is
bit<<3, so those are bits 2,1,0,3,5,6) - which assembles 0b011 = subtype 0x03, the
Ack code. The kernel builds these header words bit by bit in registers.
3b. SINTRAN header fields (wire 0-12)¶
| Off | Field | Status | Note |
|---|---|---|---|
| 0 | Marker1 0x21 |
WIRE | Always 0x21. Why — UNKNOWN. |
| 1 | Marker2 | WIRE | 0x13 normal, 0x12 relayed. |
| 2 | "PacketType" | RESOLVED - not a field | The high byte of word 1 (type:subtype), 0x00 on all 3595 frames. See below. |
| 3 | Subtype | WIRE | 0x0E Data, 0x03 Ack, 0x13/0x19 Reach, 0x07 NetworkError, plus 0x0A and 0x0C - bulk file-transfer data, decoded 2026-07-31, see below. |
| 1 | Marker2 | WIRE | 0x13 normal, 0x12 relayed - and 0xFD/0xFE on a fourth family that is NOT a subtype variant, see below. |
| 4-5 | Dest node | WIRE | |
| 6-7 | Src node | WIRE | Logical source, not the LAPB neighbour. |
| 8-9 | Flags1 | CARVED | XMSEQ (0o154). Assigned from a per-link counter, masked to 15 bits. See below. |
| 10-11 | Flags2 | CARVED | A copy of XMCSM. See section 4. |
| 12 | "Protocol ID" / channel | CARVED - SOLVED | HIGH byte of the header CHECKSUM. Not a channel at all. |
| 13 | "Counter" | CARVED - SOLVED | LOW byte of the same checksum. |
SOLVED 2026-07-31. Word 6 is a ones-complement checksum over the other six header words:
w6 == ~ones_complement_sum(w0..w5, 0)(16-bit, end-around carry). Carved at kernel137314and verified on 3595/3595 frames - every capture, every subtype, both directions. The "channel", the "epoch", and the per-link "seed" do not exist; they were artifacts of curve-fitting to checksum arithmetic. Full write-up and the code:XMSG-HEADER-WORD6-IS-A-CHECKSUM-2026-07-31.md.
4. XMCSM — carried twice, and overloaded¶
CARVED: the kernel comments it "datagram checksum; if not checksum, then message size". The corpus shows both, and they split almost evenly:
| Arm | Frames | Shape |
|---|---|---|
| size | 718 | XMCSM & 0x1FF == bodyLen exactly. High bits are 0x0 (492) or 0x2 (226) — i.e. bit 0x400 is a flag sitting above a 9-bit length. |
| not-size | 731 | XMCSM is a small constant (0x0080, 0x0064, …) while bodyLen varies. |
CARVED 2026-07-31 — the kernel comment is literally true, and here is the code. Sequence at
134013, with XMSIZ=0o144, XMSTA=0o135, XMCSM=0o142 all from the symbol list:
134013 LDA ,B 144 ; A := XMSIZ
134014 STA ,B 142 ; XMCSM := XMSIZ <- stores the SIZE; this is the default
134015 LDATX
134016 BSKP ONE 110 DA ; flag bit test
134017 JMP -> 134034 ; set -> keep the size, done
134021 BSKP ONE 120 DA ; second flag bit test
134022 JMP -> 134034 ; set -> keep the size, done
134024 LDA ,B 135 ; A := XMSTA
134025 STA -104 ; save XMSTA
134026 STZ ,B 135 ; XMSTA := 0
134027 STZ ,B 142 ; XMCSM := 0 <- zero the field before computing
134030 JPL I 36 ; -> mem[134066] = 137335
134031 STA ,B 142 ; XMCSM := result <- stores the CHECKSUM instead
134032 LDA -111
134033 STA ,B 135 ; XMSTA restored
Zeroing XMCSM (and XMSTA) before computing and storing the result back is the standard
checksum-over-block idiom, and it explains why XMSTA is excluded from the covered bytes.
The checksum itself, at 137335 - a ones-complement sum with end-around carry:
137337 LDT ,B 145 ; T := XMDAB data buffer address
137340 LDX ,B 146 ; X := XMDAW
137341 LDA ,B 147 ; A := XMLEN the LENGTH
137342 SHA ZIN SHR 1 ; A >>= 1 bytes -> words
137343 RADD CLD SA DL ; L := word count
137344 RADD CLD 0 DD ; D := 0 accumulator
137345 SKP IF DL UEQ 0 ; while L != 0
137347 LDATX ; A := mem[X+T]
137350 RADD SA DD ; D += A
137351 RADD ADC CLD SD DD; D += carry <- END-AROUND CARRY
137352 AAX 1 ; X++
137353 RADD CM1 0 DL ; L--
137354 JMP -> 137345
137355 BSKP ONE SSM ; odd-length tail, masked with AND 21
So XMLEN (0o147) is a real length field - it is what bounds the checksum - it simply is
not at wire 30-31 where our layout put it.
This also resolves what the wire measurements could not. The frames that did NOT carry a length
looked wrong
because its values are constant (0x0080, 0x0064) across frames with different bodies,
which is not checksum-like. Under the carve those constants are XMSIZ
- the message buffer size, fixed per message class - not the body length. The
XMCSM & 0x1FF == bodyLen frames are the ones where the buffer size happens to equal the body.
The discriminator: BOTH bit 9 AND bit 10 must be set to get a checksum.
nd100-dis prints the BSKP bit field as bit<<3, so 110 octal is bit 9 and 120 is
bit 10 (divide the printed octal by 8 - established by sweeping the field 0..15 through the
decoder). And BSKP skips the NEXT instruction when the condition holds, so the branch sense
is the opposite of how it first reads:
134016 BSKP ONE 110 DA ; skip next if bit 9 SET
134017 JMP -> 134034 ; taken when bit 9 CLEAR -> keep the SIZE
134021 BSKP ONE 120 DA ; skip next if bit 10 SET
134022 JMP -> 134034 ; taken when bit 10 CLEAR -> keep the SIZE
134024 ... ; both SET -> compute the CHECKSUM
So the checksum is the narrow case: it is computed only when both flags are set, and the size
is the default. That fits the corpus, where 731 of 1449 frames carry a fixed XMSIZ constant.
NARROWED 2026-07-31 (after the nd100-dis TX fix) - it is a PER-LINK property, not a message
option. The rebuilt disassembler decodes the TX displacement field (bits 3-5), which was
previously hidden: EL = (T & 0xFF)<<16 | (X + disp). That turns the opaque LDATX into a
named field access:
134004 LDA ,B 154 ; A := XMSEQ (0o154 - the datagram sequence)
134005 JAP -> 134013 ; already assigned -> skip
134006 LDATX 4 ; A := phys[X+4] the per-link counter
134007 STA ,B 154 ; XMSEQ := pre-increment value <- THIS message's sequence
134010 AAA 1 ; A += 1
134011 AND 54 ; A &= 077777 <- FIFTEEN-bit mask (constant at 134065)
134012 STATX 4 ; phys[X+4] := A write the counter back
134013 LDA ,B 144 ; A := XMSIZ
134014 STA ,B 142 ; XMCSM := XMSIZ
134015 LDATX 7 ; A := phys[X+7] <- THE FLAG WORD
So the checksum flag lives at offset 7 of the same per-link block that holds the sequence
counter at offset 4. That makes it a link/route property, not a per-message option -
which withdraws the earlier XFSEC/XFROU inference: those are message options and
cannot be what is read here.
Still UNKNOWN: what that per-link block is and what bits 9/10 of its word 7 mean. LDATX is
privileged PHYSICAL addressing via the T:X pair, so the block is at a runtime physical
address. A live DAP capture with X known at 134015 would name it immediately.
B meanwhile is the message-buffer base - confirmed because 0o132=XMLIM,
0o133=XMCUR, 0o134=XMTHD, 0o144=XMSIZ, 0o154=XMSEQ all line up with the symbol
list.
To finish this, capture it live over DAP (both machines are up), the same way the resident
DATA cells were handled - see the sintran-carving resident doc. Another static pass will not
resolve a physical address chosen at runtime.
Flags1 is a 15-bit counter [CARVED]¶
The mask at 134011 is the constant 077777 at 134065 = 0x7FFF. So the datagram
sequence increments modulo 0x8000, not 0x10000, and XMSEQ is assigned the value BEFORE
the increment (read, stash, +1, mask, write back), guarded so it happens once per message.
The corpus is consistent but cannot confirm this: the highest Flags1 on any Data frame is
0x03D4, far below either wrap point, and zero Data frames carry Flags1 >= 0x8000. The mask
is the evidence; the wire merely fails to contradict it. Anyone modelling wrap behaviour (the
envelope "epoch" term counts these wraps) must use 0x8000.
Reachability frames use 0xFFFF as a link-start sentinel and are NOT the counter.
Decoder note - fixed 2026-07-31¶
nd100-dis previously switched this group on instr & 0xFFC7, dropping the bits 3-5
displacement, so every TX instruction printed bare. The rebuilt version decodes it for all
seven variants (LDATX, LDXTX, LDDTX, LDBTX, STATX, STZTX, STDTX).
Audit of the change across the whole kernel: 116 of 236 TX instructions gained a displacement, and NO mnemonic changed - so the fix adds information rather than correcting any earlier reading, and every conclusion in this document that predates it still stands.
Bonus: this routine is the transported-header builder. At 133727, LDA 125 reads
P-relative from 134054 - a genuine literal holding 020400 = the 0x2100 marker - and
133730 stores it to XMTHD. That is the entry point for anything else about the transported
header.
This matters well beyond documentation: XmsgEnvelope.BaseLowSigned subtracts this field's low
byte, so on frames where XMCSM holds a length the envelope arithmetic is subtracting a byte
count.
3c. "PacketType" at offset 2 is not a field [RESOLVED 2026-07-31]¶
Once the header is read as seven words, bytes 2 and 3 are one word, and offset 2 is simply its
high half. Measured across the corpus: 0x00 on all 3595 frames, and word 1 takes only
eight values, all of the form 0x00XX:
0x0003 0x0007 0x000A 0x000C 0x000E 0x0013 0x0017 0x0019
- those are exactly the eight observed subtypes.
So there is no separate packet-type field. The subtype is a word whose high byte is always zero because every subtype value fits in a byte, and "PacketType" was an artifact of parsing the header as bytes. The inventory row is closed as not a field rather than left as UNKNOWN - there was never anything there to learn.
(It follows that a future subtype above 0xFF would occupy this byte, so a parser should read
the word rather than assume zero. But nothing in the corpus does.)
4b. Subtypes 0x0A / 0x0C - bulk file-transfer data [decoded 2026-07-31]¶
Found by a corpus scan that counted subtypes rather than assuming the documented five.
226 FCS-valid frames each, and they appear in only four captures - all of them
file transfers (transfer-PULL-content, transfer-SPARSE-s3config,
transfer-SMALL-167bytes, transfer-file-COMPLETE).
0x0A |
0x0C |
|
|---|---|---|
| info length | 622, always | 450, always |
| Protocol ID | 0xD8 |
0xDA |
Flags2 / XMCSM |
0x0406 |
0x0252 |
They carry a normal XMSG sub-header - XMTHD = 2100, and XMCSM (wire 26-27) equals
Flags2 exactly, as for every other frame - so the transported-header model applies to them
unchanged. Only the subtype and the fixed framing differ.
The payload is unmistakably file content. Stripping the parity bit from the high-bit ASCII gives ordinary symbol-table text with CR/LF line endings:
c5 53 56 41 d2 bd 30 30 b7 b1 36 35 -> "ESVAR=007165"
53 cc 41 4b 4b bd 30 30 30 b2 30 30 -> "SLAKK=000200"
That is a .SYMB listing being pulled across the link. So these are the bulk data path
of a file transfer, distinct from the 0x0E request/reply traffic - which is why every
earlier analysis missed them: they only occur once a transfer is actually moving content, and
every scan filtered on subtype == 0x0E.
They are a PAIR, not two message types [2026-07-31]¶
The equal counts are not a coincidence. Dumped in arrival order per direction, they strictly
alternate and every Flags1 carries both:
claude-transfer-PULL-content dir 10362 n=84 ACACACACACACACACACACACACACACACACACACACAC
Flags1: 0x1a1 0x1a1 0x1a2 0x1a2 0x1a3 0x1a3 0x1a4 0x1a4
Flags1 values carrying BOTH subtypes: 42 of 42
claude-transfer-SPARSE-s3config dir 45164 n=356 ACACACACACACACACACACACACACACACACACACACAC
Flags1 values carrying BOTH subtypes: 178 of 178
Same in all four transfer captures, in both directions. So one datagram sequence number
produces two frames - a 0x0A of 622 bytes followed by a 0x0C of 450 - and the two are
halves of a single logical transfer block rather than independent messages. Flags1 is
therefore not one-per-frame on this traffic, which is worth knowing for anything that treats
it as a frame counter.
Still UNKNOWN: why the split, and what distinguishes the halves. Note the corpus cannot
answer the arithmetic questions: XMCSM is a constant on each side (0x0406 for 0x0A,
0x0252 for 0x0C) and both frame sizes are fixed, so nothing varies and any numeric relation
between XMCSM and the body lengths holds trivially. Resolving this needs a transfer captured
with a deliberately different block size, or the *XFTRA transfer loop carved.
4c. Marker2 0xFD/0xFE - a fourth frame family, NOT a subtype [OPEN]¶
Four frames in li-rout-102-tree.pcapng, from node 103, look like "subtype 0x17" but are
nothing of the sort - Marker2 is 0xFD or 0xFE, not 0x13/0x12:
21 fe 00 17 0066 0067 ffff fffd dd + one trailing byte 0x1F
21 fd 00 17 0066 0067 ffff fffd dd + one trailing byte 0x20
Flags1 = 0xFFFF and Flags2 = 0xFFFD (a negative XE* code shape).
They are HEADER-ONLY frames [2026-07-31]. Their info length is 14 bytes = exactly seven
words, so there is no body at all - what an earlier revision of this document listed as a
"trailing byte 0x1F" is simply the low half of word 6, i.e. part of the checksum. That was a
byte-oriented mis-split, the same one that made the Counter look like a standalone field.
Their header checksum validates under the ordinary rule (verified as part of the 3595/3595 sweep), so the seven-word layout genuinely applies to them despite the unusual Marker2. That is real evidence about the family rather than an assumption.
What remains UNKNOWN is what they MEAN: Marker2 0xFD/0xFE is outside the known set, so
offset 3 (0x17 here) may not be a subtype field in this family, and Flags2 = 0xFFFD looks
like a negative XE* code but is unconfirmed. The dissector deliberately does not decode them -
see the marker-guard comment in hdlc_tcp.lua.
5. Sub-header¶
| Off | Field | Status | Note |
|---|---|---|---|
| 13 | Counter | WIRE | See section 3 - it is the LOW byte of header word 6, not a standalone byte. Earlier revisions of this document called it "between the two headers, belonging to neither"; that was the byte-oriented mis-framing. |
| 16-17 | XMSTA |
CARVED | Low byte = 5M* message state, high byte = XF* send options. Bit assignments confirmed from symbols. |
| 18-25 | XMDSY/XMDPT/XMSSY/XMSPT |
CARVED | System and port. Port = magic low word: (port << 7) | random, 5PSHZ=7, 5PMS1=177. |
| 26-27 | XMCSM |
CARVED | Section 4. |
| 28-29 | first body word | WIRE | Not header. Values are all application-layer: 0x07F0/0x07A2/0x07C0/0x07D2 (FA message types), 0x0041 XSLET, 0x014B XSGSY, 0x0100 XRSOK. |
| 30-31 | (was called XMLEN) |
UNKNOWN | XMLEN=147 octal is five words past XMCSM, so this is body, not a header length. The "16-bit XMLEN" reading needs retiring. |
5b. Offset-12 hunt: what has been RULED OUT [2026-07-31]¶
Four independent strategies, all run against XMSG-KERNEL-L03 and XMSG-XROUT-L03, all
negative. Recorded so nobody repeats them:
- Literal
0x2113/0x2112(020423/020422octal) - absent from both images, and no symbol in any symbol list carries the value. SAA 41+ shift (the marker's high byte0x21is octal41) -SAA 41occurs 5 times in XROUT and 0 times in the kernel, but every occurrence is immediately followed by a call, with neighboursSAA 17,SAA 20,SAA 35,SAA 37,SAA 10. Those are small numeric status/error codes passed in A, not a marker byte. Red herring.SHA ZIN 10+ORA(the shift-left-8 byte-packing idiom). The most promising hit was inXSDGMitself at137703:LDA ,X 16 / SHA ZIN 10 / ORA ,X 21 / RADD CLD SA DT, which builds a genuinehigh:lowword and passes it in T. It is argument packing, not a header field - the callee resolves throughmem[137745]to137515, which is a block-move helper: it rounds a byte count up to words (AAA 1 / SHA ZIN SHR 1), unpacks T's high byte at137524, and runs aMOVEWloop. The XROUT sites are similar packing.- Marker as a stored word anywhere in either image - nothing.
Taken together these strengthen the earlier conclusion considerably: the 7-word SINTRAN
header is not built by the XMSG kernel or by XROUT. XMSG builds the transported header only
(XMTHD..XMCSM), and hands the message down.
Next target: the RESIDENT image, versions/L-VSX-500/resident/SINTRAN-DATA_commoncode.bin
(a .dis already exists beside it). A first pass finds several SHA ZIN 10 sites and a word
020423 at address 001005 - which is exactly 0x2113, though it also disassembles as a
plausible STD ,B 23, so it must be confirmed by finding an instruction that references it
rather than trusted on sight.
6. Status - what is closed and what is left¶
CLOSED this session (all carved, not fitted)¶
| Item | Result |
|---|---|
| Transported header layout | 7 words, XMTHD..XMCSM, wire 14-27 |
XMCSM word size |
ONE word at 26-27; Flags2 is a copy of it (1449/1449) |
| Body start | wire offset 28 |
| SINTRAN header shape | SEVEN words; unified the Data and ACK models |
| Header offset 12 + 13 | the two halves of a ones-complement header CHECKSUM (3595/3595) |
XMCSM holds one of two things |
XMSIZ by default, or a checksum when two flag bits are set; both routines carved |
Flags1 |
XMSEQ, assigned from a per-link counter, masked to 15 bits |
| Header offset 2 | not a field - high half of the type:subtype word |
Subtypes 0x0A/0x0C |
bulk file-transfer data, and a PAIR sharing one Flags1 |
0xFD/0xFE frames |
header-only (14 bytes = 7 words); checksum validates normally |
OPEN - needs the live machines or a new capture¶
- What decides whether
XMCSMholds the size or the checksum. Narrowed to bits 9 and 10 ofphys[X+7], a word in the same per-link block whose sequence counter sits atX+4.LDATXis physical addressing, so this is a runtime address - static carving cannot finish it. A DAP breakpoint at134015withXknown would name it in one shot. - What the
0xFD/0xFEfamily means. Layout confirmed, semantics unknown. - Why
0x0A/0x0Csplit into a pair. The corpus cannot answer it:XMCSMand both frame sizes are constant, so nothing varies. Needs a transfer captured at a different block size, or the*XFTRAtransfer loop carved. - LAPB address
0x07on some ACK I-frames.
OPEN - implementation debt (no research needed, just work)¶
- Retire the 32-bit
ControlService. It straddlesXMCSMand the first body word; 108 call sites across 34 files. Documented onXmsgDataFields.ControlService. SintranProtocolIdmember names are misnomers.Tad/Routing/Padname traffic, not a selector. Renaming is a broad breaking change - flagged, not done.- Delete the superseded
XmsgEnvelopemembers (LearnSeed,BaseLow,BaseLowSigned,ComputeCounter,ComputeEpoch,DeriveChannel,ChannelAnchor) once callers migrate. - Install the fixed
hdlc_tcp.luaintoC:\Program Files\Wireshark\plugins(needs admin).
OPEN - application layer, barely started¶
- QFORM field selectors - needs the Ghidra FA handlers read.
- The COSMOS FA operations (
Open-file,Read-file,Write-file,Reserve-file-entry,SIII-special) - named incos-fa-serv-e04.progbut not decoded. - An
*XFTRAclient inXmsg.Api- does not exist;p11/p16are now known.
7. Disassembly: started, and the tool was broken¶
Target: what writes SINTRAN header offset 12. Findings so far:
- The marker
0x2113is almost certainly COMPUTED, not a literal - which is why no scan finds it. Evidence:XMTHDis built from a genuine literal0x2100(134054, stored at133730); the SINTRAN header marker is0x2113and its relayed variant is0x2112. Same high byte0x21, low byte varying with hop - that is a constructed word, so searching for the assembled value was never going to work. Look for code that builds on a0x21high byte instead. - Scanning the carved L segments for
0x2113/0x2112returned 92 word-aligned candidates with ordinary code contexts - unusable without narrowing. (S3SMPIT/S3IMPITshow a repeating2c11 2111 2c13 2113run that is clearly a table or code, not a marker.) This is the noisy-scan problem again. - Segment inventory for the next pass:
076-S3XMKis the installed XMSG kernel,077-S3XROUXROUT,036-S3TADTAD,135-XFTRADthe file-transfer daemon. There is no HDLC/network segment, so the wrapper builder is most likely in the RESIDENT code (SINTRAN-DATA_commoncode.bin), which already has a.disalongside it. - A constant scan is NOISY - verify each hit.
020400(theX5THDmarker) is also the encoding ofSTD 0,B. Of six hits in the kernel, three were ordinary instructions and134054was the genuine literal - loaded P-relative byLDA 125at133727and stored toXMTHD, which is what identified the transported-header builder. An earlier revision of this document said all six were code; that was a generalisation from checking three. Confirm each hit by finding the instruction that references it, and prefer navigating by symbol. - Useful symbols located in the kernel image:
XSDGM137601 (send datagram),XSCTR137562,XSACK140076,XSFOR137560,ZDCHN122204.
Use nd100-dis, not the Python script¶
Tool of record: /home/ronny/repos/nd100-tools/nd100-dis (WSL). A real C disassembler
with full instruction decode plus IO-device and MON-call tables. tools/nd100dis.py in this
repo is a ~140-line ad-hoc script whose own header says it is "minimal, focused on control
flow / MON / IOX / status polls" - it should not be used for carving.
nd100-dis does not recognise this BPUN variant (it falls back to raw and mis-frames the
file - reports 23833 words where the loader places 23551), so the working recipe is to
flatten first and disassemble the flat image:
python bpun2raw.py XMSG-KERNEL-L03.BPUN kernel-l03.bin # uses tools/bpun_load.py
nd100-dis -a -b 40960 -s <offset> -n <count> kernel-l03.bin
-b and -s are parsed as DECIMAL - the -o flag only affects output formatting. Base
0o120000 is -b 40960; an address A needs -s (A - 0o120000) in decimal. Passing
-b 120000 silently wraps mod 2^16 and lands you at 0o152300 with every address wrong.
Verified: -b 40960 -s 4653 puts ZCRMG at 131055, matching the published carve.
It is also more precise than the hand decodes: 174220 reads BSET ONE SSK (naming the K
flag) where the doc guessed BSET ONE 2 D0, and 146175 reads RADD CLD SX DA - which is
what COPY is an alias for.
A defect in tools/nd100dis.py, found and fixed 2026-07-31 (kept because disbpun.py
and other scripts import it). The register-operation test read
(w & 0o170000) == 0o144000, which can never be true because 0o144000 & 0o170000 is
0o140000. decode_reg was dead code and the entire ROP group fell through to the SKP
fallback - 146151 printed as SKP IF DD LSS SD where it is COPY SA DD. Any earlier
automated disassembly from that script mis-decoded every register operation. Hand decodes
in the carve docs are unaffected. The fix also corrects the ROP register table, which is
not the memory-reference one: {0:'0', 1:'D', 2:'P', 3:'B', 4:'L', 5:'A', 6:'T', 7:'X'},
calibrated against the three published hand decodes and verified to reproduce them exactly.
8. Method note¶
Everything in section 1 came from reading the kernel's own symbol table and then checking it against traffic. Nothing in it came from fitting curves to captures — an earlier attempt this session did exactly that, produced a confident and wrong explanation, and had to be withdrawn. Where a row below says WIRE rather than CARVED, that gap is the point: it marks a field we can reproduce but cannot yet explain.