code wiki / (root) / nx_h2_conformance.nx

nx_h2_conformance.nx source

↩ module page · 484 lines · 27484 B

1// nx_h2_conformance.nx -- TUTOR-BOOTSTRAP SCAFFOLD (Claude, authored under the 2// R4-H2 HTTP/2-transport-ladder workflow), NOT credited as team self-authoring. 3// 4// R4-H2-006 of the sovereign HTTP/2 transport ladder 5// (knowledge/specs/2026-06-13-http2-transport-ladder.md). The CONFORMANCE 6// harness: it composes ALL of the lower GREEN rungs -- HPACK (R4-H2-002), the 7// frame codec (R4-H2-003), the stream state machine (R4-H2-004) and flow control 8// (R4-H2-005) -- into a minimal HTTP/2 CLIENT CONNECTION SEQUENCE asserted over 9// an IN-MEMORY / loopback byte stream (NO live network, NO sockets in the gate): 10// preface -> empty SETTINGS -> HEADERS(GET, END_STREAM|END_HEADERS) 11// -> expect peer SETTINGS + our SETTINGS-ACK exchange + response HEADERS + DATA. 12// The byte sequence is the allowed internet-boundary requirement (RFC 9113 / 7541 13// dictate the exact octets so we interoperate); the IMPLEMENTATION is pure 14// NishiLang via nx_cc -> nxasm_x86: no gcc, no openssl, no nghttp2. 15// 16// FOUNDED ON (composes, does not reinvent -- anti-orphan law): 17// - nx_h2_flow.nx (R4-H2-005, GREEN): imported EXACTLY ONCE. It transitively 18// splices nx_h2_frame.nx -> nx_hpack.nx -> nx_str.nx -> syscalls.nx, so this 19// ONE import inherits the whole stack: the HPACK encoders 20// (hpack_encode_indexed / hpack_encode_lit_inc_indexed_name), the frame 21// builders (h2_write_preface / h2_frame_write_settings_empty / 22// h2_frame_write_settings_ack / h2_frame_write_headers / h2_frame_write_data / 23// h2_frame_read_header / h2_check_preface), the flow-window arithmetic 24// (h2_flow_init / h2_flow_consume / h2_flow_is_data_frame), plus sys_mmap / 25// sys_write / sys_exit / sys_openat_append / sys_close. Importing 26// nx_h2_frame / nx_hpack / nx_str / nx_syscalls / nx_h2_stream directly TOO 27// would be the RC6 double-import landmine. 28// - nx_h2_stream.nx (R4-H2-004, GREEN): imported EXACTLY ONCE for the stream 29// state machine (h2_stream_new / h2_stream_send_headers / 30// h2_stream_recv_headers / h2_stream_recv_data). nx_h2_stream imports 31// nx_h2_frame, and nx_h2_flow ALSO imports nx_h2_frame -- but each shared 32// module is included at most once by the linker (include-guarded), so 33// importing BOTH nx_h2_flow and nx_h2_stream here pulls nx_h2_frame in once. 34// No floating capability: HPACK founds the codec founds the stream machine founds 35// flow control founds THIS conformance/ALPN rung -- the top of the ladder. 36// 37// BACK-FILL: the team RE-AUTHORS this from the DATA spec via the 38// emitter-of-emitters (X-AUT-006c/e/f) -- this hand-authored scaffold is the 39// sanctioned one-time bootstrap only (meter-integrity, mirror 40// nx_frame_codec.nx:13-16 / nx_h2_frame.nx:31-34 / nx_h2_flow.nx:30-33). 41// 42// GATE (main): asserts byte-exact the composed client opening for GET "/" to 43// authority "example.com" (preface + empty SETTINGS + HEADERS wrapping the HPACK 44// block 82 87 84 41 0b example.com), then drives the IN-MEMORY descent: a 45// synthesized server response (peer SETTINGS + ACK + HEADERS :status 200 + DATA 46// END_STREAM) is fed through h2_consume_frames, which walks each frame, dispatches 47// by type, drives the stream SM to CLOSED and debits the flow windows. PLUS 48// TAMPER cases on a SEPARATE buffer (never the canonical KAT buffer): a server 49// DATA frame injected on stream 0 (illegal -- DATA MUST NOT use stream 0x0) is 50// rejected (< 0), and the ALPN parser rejects a 2-entry EE list + a list_len that 51// disagrees with ext_data_len. 52// 53// license_tier: INDEPENDENT_REDERIVE 54// genealogy_id: international-research-sources/ietf/rfc_7301 + ietf/rfc_9113 + ietf/rfc_8446 55// lineage_id: nishi_h2_conformance_r4h2_006 56 57import "nx_h2_flow.nx" 58import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 59import "nx_h2_stream.nx" 60import "nx_tls13_ext.nx" 61const K_MAGIC_1024: i64 = 1024 62const K_MAGIC_65535: i64 = 65535 63const K_MAGIC_65533: i64 = 65533 64 65// ---- print helpers (the h2_puts/h2_putn pattern, renamed cf_*) ---- 66func cf_puts(s: *u8) -> i64 { 67 var n: i64 = 0 68 while s[n] != (0 as u8) { n = n + 1 } 69 sys_write(1, s, n) 70 return 0 71} 72// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 73// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 74// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 75// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 76func cf_putn(v: i64) -> i64 { nxi_out(v); return 0 } 77func cf_puthex2(v: i64) -> i64 { 78 let h: *u8 = sys_mmap(4) 79 let d0: i64 = (v >> 4) & 0xf 80 let d1: i64 = v & 0xf 81 if d0 < 10 { h[0] = (48 + d0) as u8 } else { h[0] = (87 + d0) as u8 } 82 if d1 < 10 { h[1] = (48 + d1) as u8 } else { h[1] = (87 + d1) as u8 } 83 sys_write(1, h, 2) 84 return 0 85} 86// fd-aware decimal writer (the h2_fdn pattern) -- used ONLY by the durable 87// status-line append at the end of the gate. 88// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 89// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 90// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 91// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 92func cf_fdn(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 } 93 94// ===================================================================== 95// Build the HPACK header block for a client GET: encode the four pseudo-headers 96// :method GET (indexed 82) 97// :scheme https (indexed 87) 98// :path <path> (indexed 84 when path == "/", else literal indexed-name 4) 99// :authority <authority> (6.2.1 indexed-name, name index 1 -> lead 0x41) 100// Returns the block length, or < 0 on encode error. COMPOSES nx_hpack. 101// ===================================================================== 102func h2_build_request_block(block: *u8, authority: *u8, alen: i64, path: *u8, plen: i64) -> i64 { 103 var bo: i64 = hpack_encode_indexed(block, 0, 2) // 82 :method GET 104 if bo < 0 { return bo } 105 bo = hpack_encode_indexed(block, bo, 7) // 87 :scheme https 106 if bo < 0 { return bo } 107 // :path -- "/" is static index 4 (indexed); anything else is a 6.2.2 108 // literal w/o indexing on :path (name index 4) carrying the path value. 109 if plen == 1 { 110 if (path[0] & 0xff) == 0x2f { // '/' 111 bo = hpack_encode_indexed(block, bo, 4) // 84 :path / 112 if bo < 0 { return bo } 113 } 114 if (path[0] & 0xff) != 0x2f { 115 bo = hpack_encode_lit_noindex_indexed_name(block, bo, 4, path, plen) 116 if bo < 0 { return bo } 117 } 118 } 119 if plen != 1 { 120 bo = hpack_encode_lit_noindex_indexed_name(block, bo, 4, path, plen) 121 if bo < 0 { return bo } 122 } 123 // :authority -- 6.2.1 literal w/ incremental indexing, name index 1 (0x41). 124 bo = hpack_encode_lit_inc_indexed_name(block, bo, 1, authority, alen) 125 if bo < 0 { return bo } 126 return bo 127} 128 129// ===================================================================== 130// Build the minimal client opening into `out` at `off`: 131// 24-byte preface + empty client SETTINGS + HEADERS(stream 1, flags 132// END_STREAM|END_HEADERS = 0x05) wrapping the HPACK GET block. 133// Returns the total byte count (new offset), or < 0 on error. This is the 134// COMPOSITION of R4-H2-003 (preface/SETTINGS/HEADERS builders) + R4-H2-002 135// (HPACK block). The HEADERS frame transitions the R4-H2-004 stream IDLE -> 136// half-closed(local) (drven separately in the gate via the stream SM). 137// ===================================================================== 138func h2_build_client_open(out: *u8, off: i64, authority: *u8, alen: i64, path: *u8, plen: i64) -> i64 { 139 var o: i64 = h2_write_preface(out, off) // +24 140 o = h2_frame_write_settings_empty(out, o) // +9 (00 00 00 04 00 00 00 00 00) 141 if o < 0 { return o } 142 let block: *u8 = sys_mmap(128) 143 let blen: i64 = h2_build_request_block(block, authority, alen, path, plen) 144 if blen < 0 { return blen } 145 o = h2_frame_write_headers(out, o, 1, 0x05, block, blen) // HEADERS stream 1, END_STREAM|END_HEADERS 146 if o < 0 { return o } 147 return o 148} 149 150// ===================================================================== 151// UA-carrying request block + open (NBC/IMGSEARCH: many origins -- google image 152// search among them -- serve a JS "enable JavaScript / update your browser" wall 153// to clients that send NO user-agent; an IDENTIFIED desktop UA gets the real 154// results HTML). ADDITIVE (rule 19): same four pseudo-headers as h2_build_request_block 155// + one user-agent header (HPACK static name index 58, literal value, no indexing). 156// ===================================================================== 157func h2_build_request_block_ua(block: *u8, authority: *u8, alen: i64, path: *u8, plen: i64, ua: *u8, ualen: i64) -> i64 { 158 var bo: i64 = h2_build_request_block(block, authority, alen, path, plen) 159 if bo < 0 { return bo } 160 bo = hpack_encode_lit_noindex_indexed_name(block, bo, 58, ua, ualen) // user-agent 161 if bo < 0 { return bo } 162 return bo 163} 164func h2_build_client_open_ua(out: *u8, off: i64, authority: *u8, alen: i64, path: *u8, plen: i64, ua: *u8, ualen: i64) -> i64 { 165 var o: i64 = h2_write_preface(out, off) 166 o = h2_frame_write_settings_empty(out, o) 167 if o < 0 { return o } 168 let block: *u8 = sys_mmap(K_MAGIC_1024) 169 let blen: i64 = h2_build_request_block_ua(block, authority, alen, path, plen, ua, ualen) 170 if blen < 0 { return blen } 171 o = h2_frame_write_headers(out, o, 1, 0x05, block, blen) 172 if o < 0 { return o } 173 return o 174} 175 176// ===================================================================== 177// Walk a server frame stream over an in-memory buffer [off, lim). Dispatch each 178// frame by type, drive the per-stream state machine and the flow windows, and 179// REJECT illegal framing. Returns the number of frames consumed, or < 0 on a 180// protocol error. COMPOSES the codec (read-header), the stream SM (recv_*), and 181// flow control (consume / is_data_frame). 182// 183// Rejections (RFC 9113 §6.1 / §5.1): 184// - a DATA frame on stream 0 (DATA MUST NOT use stream 0x0) -> -1 185// - a header that runs past the buffer -> propagated < 0 186// - a stream-frame (HEADERS/DATA) whose SM transition is 187// illegal for the current state -> -1 188// - SETTINGS / WINDOW_UPDATE on a non-zero stream where the 189// spec requires stream 0 is NOT enforced here (connection 190// control frames are accepted on stream 0 only by checking 191// DATA/HEADERS parity); the headline illegal-stream-0 check 192// is the DATA-on-0 tamper case. 193// ===================================================================== 194func h2_consume_frames(buf: *u8, off: i64, lim: i64, conn_window: *i64, stream_window: *i64, stream_state: *i64) -> i64 { 195 var o: i64 = off 196 var frames: i64 = 0 197 let rlen: *i64 = sys_mmap(16) as *i64 198 let rtype: *i64 = sys_mmap(16) as *i64 199 let rflags: *i64 = sys_mmap(16) as *i64 200 let rsid: *i64 = sys_mmap(16) as *i64 201 while o < lim { 202 let payload_start: i64 = h2_frame_read_header(buf, o, lim, rlen, rtype, rflags, rsid) 203 if payload_start < 0 { return payload_start } // truncated / over-long -> propagate 204 let ftype: i64 = rtype[0] 205 let flags: i64 = rflags[0] 206 let sid: i64 = rsid[0] 207 let plen: i64 = rlen[0] 208 let end_stream: i64 = flags & 0x01 209 210 if ftype == 0x00 { // DATA (§6.1) 211 if sid == 0 { return 0 - 1 } // DATA MUST NOT use stream 0 -> PROTOCOL_ERROR 212 // flow control: a DATA frame debits BOTH the connection and stream windows. 213 if h2_flow_is_data_frame(ftype) == 1 { 214 let cc: i64 = h2_flow_consume(conn_window, plen) 215 if cc < 0 { return cc } // connection-window overdraw 216 let sc: i64 = h2_flow_consume(stream_window, plen) 217 if sc < 0 { return sc } // stream-window overdraw 218 } 219 let ns_d: i64 = h2_stream_recv_data(stream_state[0], end_stream) 220 if ns_d < 0 { return ns_d } // illegal transition 221 stream_state[0] = ns_d 222 } 223 if ftype == 0x01 { // HEADERS (§6.2) 224 if sid == 0 { return 0 - 1 } // HEADERS MUST NOT use stream 0 225 let ns_h: i64 = h2_stream_recv_headers(stream_state[0], end_stream) 226 if ns_h < 0 { return ns_h } // illegal transition 227 stream_state[0] = ns_h 228 } 229 // SETTINGS (0x04), WINDOW_UPDATE (0x08), PING (0x06) etc. are connection 230 // control frames -- accepted on stream 0; they do not drive the stream SM 231 // and (per §5.2.1) consume zero flow window. No state change needed for 232 // the client-GET descent beyond acknowledging them as walked. 233 234 o = payload_start + plen 235 frames = frames + 1 236 } 237 return frames 238} 239 240// ===================================================================== 241// GATE (main): composed byte-exact KAT + in-memory descent + tamper cases. 242// ===================================================================== 243func cf_check_bytes(name: *u8, got: *u8, glen: i64, exp: *u8, elen: i64) -> i64 { 244 if glen != elen { 245 cf_puts(" FAIL " as *u8); cf_puts(name) 246 cf_puts(" length got=" as *u8); cf_putn(glen) 247 cf_puts(" exp=" as *u8); cf_putn(elen); cf_puts("\n" as *u8) 248 return 0 249 } 250 var i: i64 = 0 251 while i < glen { 252 if (got[i] & 0xff) != (exp[i] & 0xff) { 253 cf_puts(" FAIL " as *u8); cf_puts(name) 254 cf_puts(" byte[" as *u8); cf_putn(i) 255 cf_puts("] got=" as *u8); cf_puthex2(got[i] & 0xff) 256 cf_puts(" exp=" as *u8); cf_puthex2(exp[i] & 0xff); cf_puts("\n" as *u8) 257 return 0 258 } 259 i = i + 1 260 } 261 cf_puts(" PASS " as *u8); cf_puts(name) 262 cf_puts(" (" as *u8); cf_putn(glen); cf_puts(" bytes)\n" as *u8) 263 return 1 264} 265func cf_check(name: *u8, got: i64, exp: i64) -> i64 { 266 if got == exp { 267 cf_puts(" PASS " as *u8); cf_puts(name) 268 cf_puts(" (== " as *u8); cf_putn(exp); cf_puts(")\n" as *u8) 269 return 1 270 } 271 cf_puts(" FAIL " as *u8); cf_puts(name) 272 cf_puts(" got=" as *u8); cf_putn(got) 273 cf_puts(" exp=" as *u8); cf_putn(exp); cf_puts("\n" as *u8) 274 return 0 275} 276 277func main() -> i64 { 278 var pass: i64 = 0 279 var tot: i64 = 0 280 cf_puts("nx_h2_conformance gate (RFC 7301/9113/8446, FOUNDED on hpack+frame+stream+flow+ext)\n" as *u8) 281 282 let IDLE: i64 = 0 283 let HC_LOCAL: i64 = 2 284 let CLOSED: i64 = 4 285 286 // ================================================================= 287 // PART A -- ALPN-h2 extension emitter byte-exact KAT (18 bytes). 288 // 00 10 00 0e 00 0c 02 68 32 08 68 74 74 70 2f 31 2e 31 289 // ================================================================= 290 let aout: *u8 = sys_mmap(64) 291 let aexp: *u8 = sys_mmap(64) 292 let an: i64 = tls13_ext_emit_alpn_h2_http11(aout, 64) 293 aexp[0]=0x00 as u8; aexp[1]=0x10 as u8; aexp[2]=0x00 as u8; aexp[3]=0x0e as u8 294 aexp[4]=0x00 as u8; aexp[5]=0x0c as u8; aexp[6]=0x02 as u8; aexp[7]=0x68 as u8 295 aexp[8]=0x32 as u8; aexp[9]=0x08 as u8; aexp[10]=0x68 as u8; aexp[11]=0x74 as u8 296 aexp[12]=0x74 as u8; aexp[13]=0x70 as u8; aexp[14]=0x2f as u8; aexp[15]=0x31 as u8 297 aexp[16]=0x2e as u8; aexp[17]=0x31 as u8 298 pass = pass + cf_check_bytes("ALPN ['h2','http/1.1'] extension" as *u8, aout, an, aexp, 18); tot = tot + 1 299 300 // ---- Part A parser: EE ALPN selecting h2. ext_data (past type+len) is 301 // 00 03 02 68 32 (len 5) -> out_ptr -> offset 3 ("68 32"), out_len 2. ---- 302 let ee: *u8 = sys_mmap(16) 303 ee[0]=0x00 as u8; ee[1]=0x03 as u8; ee[2]=0x02 as u8; ee[3]=0x68 as u8; ee[4]=0x32 as u8 304 let sp: *i64 = sys_mmap(16) as *i64 305 let sl: *i64 = sys_mmap(16) as *i64 306 let prc: i64 = tls13_ext_parse_alpn_selected(ee, 5, sp, sl) 307 var parse_ok: i64 = 0 308 if prc == 1 { // NX_TLS13_EXT_VERDICT_OK == 1 309 if sl[0] == 2 { 310 if (ee[sp[0]] & 0xff) == 0x68 { if (ee[sp[0] + 1] & 0xff) == 0x32 { parse_ok = 1 } } 311 } 312 } 313 if parse_ok == 1 { cf_puts(" PASS parse EE ALPN -> selected 'h2' (ptr=" as *u8); cf_putn(sp[0]); cf_puts(" len=2)\n" as *u8); pass = pass + 1 } 314 if parse_ok == 0 { cf_puts(" FAIL parse EE ALPN (rc=" as *u8); cf_putn(prc); cf_puts(" len=" as *u8); cf_putn(sl[0]); cf_puts(")\n" as *u8) } 315 tot = tot + 1 316 317 // ================================================================= 318 // PART B -- composed client opening for GET "/" to authority "example.com". 319 // Canonical buffer `co` is built ONCE and NEVER mutated (tamper buffers are 320 // separate, defect-(c) discipline). 321 // ================================================================= 322 let co: *u8 = sys_mmap(256) 323 let authority: *u8 = sys_mmap(32); nx_str_cpy(authority, "example.com" as *u8) 324 let path: *u8 = sys_mmap(8); nx_str_cpy(path, "/" as *u8) 325 let total: i64 = h2_build_client_open(co, 0, authority, 11, path, 1) 326 327 // Expected = preface(24) + empty SETTINGS(9) + HEADERS(9 + 16 HPACK) = 58. 328 // HPACK block (16): 82 87 84 41 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d 329 // (82 :method GET, 87 :scheme https, 84 :path /, 41 0b "example.com") 330 // HEADERS skeleton: 00 00 10 01 05 00 00 00 01 (len=0x10=16) 331 let coexp: *u8 = sys_mmap(256) 332 // preface 333 coexp[0]=0x50 as u8; coexp[1]=0x52 as u8; coexp[2]=0x49 as u8; coexp[3]=0x20 as u8 334 coexp[4]=0x2a as u8; coexp[5]=0x20 as u8; coexp[6]=0x48 as u8; coexp[7]=0x54 as u8 335 coexp[8]=0x54 as u8; coexp[9]=0x50 as u8; coexp[10]=0x2f as u8; coexp[11]=0x32 as u8 336 coexp[12]=0x2e as u8; coexp[13]=0x30 as u8; coexp[14]=0x0d as u8; coexp[15]=0x0a as u8 337 coexp[16]=0x0d as u8; coexp[17]=0x0a as u8; coexp[18]=0x53 as u8; coexp[19]=0x4d as u8 338 coexp[20]=0x0d as u8; coexp[21]=0x0a as u8; coexp[22]=0x0d as u8; coexp[23]=0x0a as u8 339 // empty SETTINGS 340 coexp[24]=0x00 as u8; coexp[25]=0x00 as u8; coexp[26]=0x00 as u8; coexp[27]=0x04 as u8 341 coexp[28]=0x00 as u8; coexp[29]=0x00 as u8; coexp[30]=0x00 as u8; coexp[31]=0x00 as u8 342 coexp[32]=0x00 as u8 343 // HEADERS skeleton (len=0x10=16, type=01, flags=05, sid=1) 344 coexp[33]=0x00 as u8; coexp[34]=0x00 as u8; coexp[35]=0x10 as u8; coexp[36]=0x01 as u8 345 coexp[37]=0x05 as u8; coexp[38]=0x00 as u8; coexp[39]=0x00 as u8; coexp[40]=0x00 as u8 346 coexp[41]=0x01 as u8 347 // HPACK block: 82 87 84 41 0b "example.com" 348 coexp[42]=0x82 as u8; coexp[43]=0x87 as u8; coexp[44]=0x84 as u8 349 coexp[45]=0x41 as u8; coexp[46]=0x0b as u8 350 coexp[47]=0x65 as u8; coexp[48]=0x78 as u8; coexp[49]=0x61 as u8; coexp[50]=0x6d as u8 351 coexp[51]=0x70 as u8; coexp[52]=0x6c as u8; coexp[53]=0x65 as u8; coexp[54]=0x2e as u8 352 coexp[55]=0x63 as u8; coexp[56]=0x6f as u8; coexp[57]=0x6d as u8 353 pass = pass + cf_check_bytes("client-open GET / example.com (preface+SETTINGS+HEADERS)" as *u8, co, total, coexp, 58); tot = tot + 1 354 355 // ---- the prefix MUST start with the 24-byte preface (h2_check_preface == 1) ---- 356 pass = pass + cf_check("client-open starts with valid preface" as *u8, h2_check_preface(co, 0, 24), 1); tot = tot + 1 357 358 // ---- the request stream transitions IDLE -> HC_LOCAL on send HEADERS(END_STREAM) ---- 359 var cs: i64 = h2_stream_new(1) 360 pass = pass + cf_check("stream new(1) == IDLE" as *u8, cs, IDLE); tot = tot + 1 361 cs = h2_stream_send_headers(cs, 1) // END_STREAM set -> HC_LOCAL 362 pass = pass + cf_check("send HEADERS(END_STREAM) -> HC_LOCAL" as *u8, cs, HC_LOCAL); tot = tot + 1 363 364 // ================================================================= 365 // FULL IN-MEMORY DESCENT -- synthesize the server response and feed it to 366 // h2_consume_frames: peer SETTINGS + our SETTINGS-ACK + response HEADERS 367 // (:status 200 = HPACK 0x88) + DATA "OK" END_STREAM. The walk drives the 368 // stream HC_LOCAL -> CLOSED and debits the windows. 369 // ================================================================= 370 let resp: *u8 = sys_mmap(256) 371 var ro: i64 = 0 372 // peer SETTINGS (empty) -- connection control, stream 0 373 ro = h2_frame_write_settings_empty(resp, ro) // +9 374 // our SETTINGS-ACK echoed in the stream (connection control, stream 0) 375 ro = h2_frame_write_settings_ack(resp, ro) // +9 376 // response HEADERS on stream 1, END_HEADERS only (no END_STREAM): HPACK :status 200 377 let hblk: *u8 = sys_mmap(16); hblk[0] = 0x88 as u8 // indexed :status 200 378 ro = h2_frame_write_headers(resp, ro, 1, 0x04, hblk, 1) // flags END_HEADERS=0x04 379 // response DATA on stream 1 with END_STREAM: body "OK" 380 let dbody: *u8 = sys_mmap(8); dbody[0]=0x4f as u8; dbody[1]=0x4b as u8 // "OK" 381 ro = h2_frame_write_data(resp, ro, 1, 0x01, dbody, 2) // END_STREAM=0x01 382 383 // windows + stream state (continuing from cs == HC_LOCAL above) 384 let connw: *i64 = sys_mmap(16) as *i64 385 let strmw: *i64 = sys_mmap(16) as *i64 386 h2_flow_init(connw) // K_MAGIC_65535 387 h2_flow_init(strmw) // K_MAGIC_65535 388 let sstate: *i64 = sys_mmap(16) as *i64 389 sstate[0] = cs // HC_LOCAL 390 391 let nframes: i64 = h2_consume_frames(resp, 0, ro, connw, strmw, sstate) 392 // 4 frames consumed: SETTINGS, SETTINGS-ACK, HEADERS, DATA. 393 pass = pass + cf_check("consume server response -> 4 frames" as *u8, nframes, 4); tot = tot + 1 394 // stream driven HC_LOCAL -> (recv HEADERS no-END stays HC_LOCAL) -> (recv DATA END_STREAM) CLOSED 395 pass = pass + cf_check("descent: stream ends CLOSED" as *u8, sstate[0], CLOSED); tot = tot + 1 396 // DATA "OK" (2 bytes) debited both windows: 65535 - 2 = 65533 397 pass = pass + cf_check("descent: conn window 65535 - 2 = 65533" as *u8, connw[0], K_MAGIC_65533); tot = tot + 1 398 pass = pass + cf_check("descent: stream window 65535 - 2 = 65533" as *u8, strmw[0], K_MAGIC_65533); tot = tot + 1 399 400 // sanity: canonical client-open buffer NOT mutated by the descent 401 pass = pass + cf_check("KAT-untouched: client-open still valid preface" as *u8, h2_check_preface(co, 0, 24), 1); tot = tot + 1 402 403 // ================================================================= 404 // TAMPER CASES -- each on a SEPARATE buffer; the canonical buffers (co, 405 // resp) are NEVER mutated in place (defect-(c) discipline). 406 // ================================================================= 407 408 // ---- TAMPER 1: a server DATA frame on stream 0 (illegal -- DATA MUST NOT 409 // use stream 0x0). Build it directly into a SEPARATE buffer via the raw 410 // frame-header writer (the safe builder refuses stream 0, so we craft 411 // the illegal bytes ourselves) and feed it to h2_consume_frames. ---- 412 let tbuf: *u8 = sys_mmap(64) 413 // 9-byte header: len=2, type=DATA(0x00), flags=END_STREAM(0x01), stream id 0 414 var to: i64 = h2_frame_write_header(tbuf, 0, 2, 0x00, 0x01, 0) 415 tbuf[to] = 0x4f as u8; tbuf[to + 1] = 0x4b as u8 // "OK" payload 416 to = to + 2 417 let tconn: *i64 = sys_mmap(16) as *i64 418 let tstrm: *i64 = sys_mmap(16) as *i64 419 h2_flow_init(tconn); h2_flow_init(tstrm) 420 let tstate: *i64 = sys_mmap(16) as *i64; tstate[0] = HC_LOCAL 421 let data0_rc: i64 = h2_consume_frames(tbuf, 0, to, tconn, tstrm, tstate) 422 var t1ok: i64 = 0 423 if data0_rc < 0 { t1ok = 1 } 424 if t1ok == 1 { cf_puts(" PASS tamper DATA-on-stream-0 rejected (rc=" as *u8); cf_putn(data0_rc); cf_puts(")\n" as *u8); pass = pass + 1 } 425 if t1ok == 0 { cf_puts(" FAIL tamper DATA-on-stream-0 NOT rejected (rc=" as *u8); cf_putn(data0_rc); cf_puts(")\n" as *u8) } 426 tot = tot + 1 427 428 // ---- TAMPER 2: ALPN EE with TWO ProtocolName entries (RFC 7301 mandates 429 // exactly one). ext_data = 00 06 02 68 32 02 68 32 (list_len 6, two 430 // "h2" entries) -> the single-entry parser MUST reject (1 + name_len != 431 // list_len because the first entry only consumes 3 of 6). ---- 432 let ee2: *u8 = sys_mmap(16) 433 ee2[0]=0x00 as u8; ee2[1]=0x06 as u8 // list_len = 6 434 ee2[2]=0x02 as u8; ee2[3]=0x68 as u8; ee2[4]=0x32 as u8 // entry 1: "h2" 435 ee2[5]=0x02 as u8; ee2[6]=0x68 as u8; ee2[7]=0x32 as u8 // entry 2: "h2" (illegal) 436 let sp2: *i64 = sys_mmap(16) as *i64 437 let sl2: *i64 = sys_mmap(16) as *i64 438 let two_rc: i64 = tls13_ext_parse_alpn_selected(ee2, 8, sp2, sl2) 439 var t2ok: i64 = 0 440 if two_rc < 0 { t2ok = 1 } 441 if t2ok == 1 { cf_puts(" PASS tamper ALPN-two-entries rejected (rc=" as *u8); cf_putn(two_rc); cf_puts(")\n" as *u8); pass = pass + 1 } 442 if t2ok == 0 { cf_puts(" FAIL tamper ALPN-two-entries NOT rejected (rc=" as *u8); cf_putn(two_rc); cf_puts(")\n" as *u8) } 443 tot = tot + 1 444 445 // ---- TAMPER 3: ALPN EE list_len != ext_data_len - 2 (framing mismatch). 446 // ext_data = 00 09 02 68 32 (claims list_len 9 but only 3 bytes follow, 447 // ext_data_len 5) -> rejected. ---- 448 let ee3: *u8 = sys_mmap(16) 449 ee3[0]=0x00 as u8; ee3[1]=0x09 as u8; ee3[2]=0x02 as u8; ee3[3]=0x68 as u8; ee3[4]=0x32 as u8 450 let sp3: *i64 = sys_mmap(16) as *i64 451 let sl3: *i64 = sys_mmap(16) as *i64 452 let mism_rc: i64 = tls13_ext_parse_alpn_selected(ee3, 5, sp3, sl3) 453 var t3ok: i64 = 0 454 if mism_rc < 0 { t3ok = 1 } 455 if t3ok == 1 { cf_puts(" PASS tamper ALPN-list_len-mismatch rejected (rc=" as *u8); cf_putn(mism_rc); cf_puts(")\n" as *u8); pass = pass + 1 } 456 if t3ok == 0 { cf_puts(" FAIL tamper ALPN-list_len-mismatch NOT rejected (rc=" as *u8); cf_putn(mism_rc); cf_puts(")\n" as *u8) } 457 tot = tot + 1 458 459 cf_puts("---- h2_conformance gate: passed " as *u8); cf_putn(pass); cf_puts(" / " as *u8); cf_putn(tot); cf_puts("\n" as *u8) 460 if pass == tot { 461 // DURABLE EVIDENCE (mirror nx_h2_frame.nx:586 / nx_h2_flow.nx:416): 462 // stdout evaporates and cannot anchor a row_markers entry -- append ONE 463 // marker/reconcile-recognised status line. Reached ONLY when every KAT 464 // + all three tamper cases pass (pass == tot). 465 let lfd: i64 = sys_openat_append("knowledge/status/h2_nx_h2_conformance.log" as *u8, 0x1a4) 466 if lfd >= 0 { 467 sys_write(lfd, "R4-H2-006-GATE organ=nx_h2_conformance kats=" as *u8, 44) 468 cf_fdn(lfd, pass); sys_write(lfd, "/" as *u8, 1); cf_fdn(lfd, tot) 469 sys_write(lfd, " tamper=ok verdict=GREEN\n" as *u8, 25) 470 sys_close(lfd) 471 } 472 sys_exit(0) 473 } 474 // RED path: still write a durable line so the failure is anchored, then exit 1. 475 let rfd: i64 = sys_openat_append("knowledge/status/h2_nx_h2_conformance.log" as *u8, 0x1a4) 476 if rfd >= 0 { 477 sys_write(rfd, "R4-H2-006-GATE organ=nx_h2_conformance kats=" as *u8, 44) 478 cf_fdn(rfd, pass); sys_write(rfd, "/" as *u8, 1); cf_fdn(rfd, tot) 479 sys_write(rfd, " tamper=?? verdict=RED\n" as *u8, 23) 480 sys_close(rfd) 481 } 482 sys_exit(1) 483 return 0 484}