code wiki / (root) / nx_https_truncation_gate.nx

nx_https_truncation_gate.nx source

↩ module page · 184 lines · 10979 B

1// nx_https_truncation_gate.nx -- teeth for the TLS-1.3 short-body check in nx_https_get_complete.nx. 2// 3// THE DEFECT IT GUARDS: nx_https_req_complete computed body_target from the peer's Content-Length and 4// then IGNORED IT at three of its four termination exits (record EOF, payload EOF, close_notify alert), 5// so a body the peer cut short returned as a POSITIVE byte count and every caller read positive + 200 6// as success. The TLS-1.2 sibling leg has checked the same number since 2026-08-18; this gate exists so 7// the 1.3 leg cannot silently lose it again. 8// 9// IN-PROCESS BY CONSTRUCTION: it imports nx_https_get_complete.nx and calls nx_https_body_state, 10// nx_https_body_shortfall and _gc_body_seal DIRECTLY, so it exercises the same code the transport runs 11// rather than parsing a fetcher stdout. That also means it has no NOT-DEPLOYED failure mode -- a gate 12// that forks a deployed elf reports INCONCLUSIVE when the subject is merely un-promoted, which reads as 13// the gate own fault. 14// 15// FIXTURES ARE BYTE BUFFERS ASSEMBLED IN THIS PROCESS, AND THAT IS THE STRONGER FORM OF THE 16// /tmp/<gate>/ LAW, NOT AN EXEMPTION FROM IT. The rule exists because a gate must not share its fixture 17// with a production beat; a buffer that lives and dies inside one process shares nothing with anything, 18// and there is no /tmp file here because there is no FILE anywhere in the code path under test. A gate 19// that wrote a file purely so it could read it back would be testing the filesystem, not the predicate. 20// Every buffer is rebuilt from scratch per tooth, so the gate is idempotent by construction. 21// 22// THE FIRST TOOTH IS THE ANTI-VACUITY ONE. The trivial wrong implementation is "call everything 23// truncated", and it would pass every detection tooth below. Only the positive controls -- complete 24// bodies that must NOT be flagged -- can refute it. A GUARD THAT REFUSES EVERYTHING PASSES EVERY 25// NEGATIVE TEST. 26// license_tier: ORIGINAL expect_exit: 0 27import "nx_https_get_complete.nx" 28import "nx_gate_verdict.nx" 29 30// Append a NUL-terminated literal; returns the new offset. DERIVED length, never hand-counted. 31func hg_cat(buf: *u8, off: i64, s: *u8) -> i64 { 32 var a: i64 = off 33 var i: i64 = 0 34 while s[i] != (0 as u8) { buf[a] = s[i]; a = a + 1; i = i + 1 } 35 return a 36} 37// CRLF is CONSTRUCTED from its byte values, never written as a literal: a bare carriage return inside 38// an nx string literal is exactly the ambiguity that has silently produced unseparated output before. 39func hg_crlf(buf: *u8, off: i64) -> i64 { 40 var a: i64 = off 41 buf[a] = 13 as u8; a = a + 1 42 buf[a] = 10 as u8; a = a + 1 43 return a 44} 45// Assemble a real HTTP/1.1 response: status line, Content-Length: <declared>, blank line, then 46// bodylen body bytes. Returns the TOTAL length written. This is the wire the parser actually sees. 47// xprefix=1 writes X-Content-Length instead, for the header-match neg-control. 48func hg_resp(buf: *u8, declared: *u8, bodylen: i64, xprefix: i64) -> i64 { 49 var a: i64 = 0 50 a = hg_cat(buf, a, "HTTP/1.1 200 OK" as *u8); a = hg_crlf(buf, a) 51 a = hg_cat(buf, a, "Content-Type: text/html" as *u8); a = hg_crlf(buf, a) 52 if xprefix == 1 { a = hg_cat(buf, a, "X-" as *u8) } 53 a = hg_cat(buf, a, "Content-Length: " as *u8) 54 a = hg_cat(buf, a, declared); a = hg_crlf(buf, a) 55 a = hg_crlf(buf, a) 56 var i: i64 = 0 57 while i < bodylen { buf[a] = 65 as u8; a = a + 1; i = i + 1 } 58 return a 59} 60// Derive body_target the way the transport does: end-of-headers + 4, plus the declared Content-Length. 61// 0 when no Content-Length was parsed -- the UNJUDGEABLE input. 62func hg_target(buf: *u8, total: i64) -> i64 { 63 let he: i64 = _gc_hdr_end(buf, total) 64 if he < 0 { return 0 } 65 let cl: i64 = _gc_clen(buf, he) 66 if cl < 0 { return 0 } 67 return he + 4 + cl 68} 69 70func main(argc: i64, argv: *i64) -> i64 { 71 gv_head("=== NX-HTTPS-TRUNCATION-GATE -- can the TLS-1.3 leg tell a cut body from a whole one, BOTH ways? ===" as *u8) 72 let ctr: *i64 = gv_ctr() 73 74 // ---------- ANTI-VACUITY FIRST: the call-everything-truncated implementation dies here ---------- 75 // Three bodies that ARRIVED IN FULL. Exact arrival, over-delivery (a keep-alive tail after the 76 // body), and a one-byte body. None of them may be called TRUNCATED. 77 var allgood: i64 = 0 78 if nx_https_body_state(1000, 1000) == NX_HTTPS_BODY_COMPLETE { 79 if nx_https_body_state(1000, 1200) == NX_HTTPS_BODY_COMPLETE { 80 if nx_https_body_state(1, 1) == NX_HTTPS_BODY_COMPLETE { allgood = 1 } } } 81 gv_check("neg-control-anti-vacuity-three-COMPLETE-bodies-are-NOT-flagged" as *u8, allgood, ctr) 82 83 // ---------- the real defect shape, both directions ---------- 84 var bad_s: i64 = 0 85 if nx_https_body_state(1000, 900) == NX_HTTPS_BODY_TRUNCATED { bad_s = 1 } 86 var good_s: i64 = 1 87 if nx_https_body_state(1000, 1000) == NX_HTTPS_BODY_COMPLETE { good_s = 0 } 88 gv_bite("short-body-is-named-TRUNCATED" as *u8, bad_s, good_s, ctr) 89 90 // A ONE-BYTE SHORTFALL IS THE WHOLE DEFECT AT ITS SMALLEST. The measured witness that started this 91 // was two copies of one document cut 22 bytes apart, so the boundary case is the interesting one. 92 var bad_1: i64 = 0 93 if nx_https_body_state(1000, 999) == NX_HTTPS_BODY_TRUNCATED { bad_1 = 1 } 94 gv_bite("a-one-byte-shortfall-still-fires" as *u8, bad_1, good_s, ctr) 95 96 // ---------- the reason must travel with the verdict, as a NUMBER ---------- 97 var bad_n: i64 = 0 98 if nx_https_body_shortfall(1000, 900) == 100 { bad_n = 1 } 99 var good_n: i64 = 1 100 if nx_https_body_shortfall(1000, 1000) == 0 { good_n = 0 } 101 gv_bite("shortfall-carries-the-byte-count-not-just-a-flag" as *u8, bad_n, good_n, ctr) 102 103 // ---------- ABSTAIN, NEVER ACQUIT ---------- 104 // No Content-Length reached us (chunked, or read-to-close). "Complete" is NOT derivable from that, 105 // and reporting COMPLETE would be a false proof with an authoritative name. 106 var unj: i64 = 0 107 if nx_https_body_state(0, 5000) == NX_HTTPS_BODY_UNJUDGEABLE { unj = 1 } 108 gv_check("neg-control-no-content-length-is-UNJUDGEABLE-and-NOT-counted-COMPLETE" as *u8, unj, ctr) 109 var unj0: i64 = 0 110 if nx_https_body_shortfall(0, 5000) == 0 { unj0 = 1 } 111 gv_check("neg-control-unjudgeable-reports-zero-shortfall-so-it-cannot-read-as-damage" as *u8, unj0, ctr) 112 113 // A state that cannot say "I never looked" reports absence of evidence as evidence of absence. 114 var distinct: i64 = 0 115 if NX_HTTPS_BODY_COMPLETE != NX_HTTPS_BODY_TRUNCATED { 116 if NX_HTTPS_BODY_TRUNCATED != NX_HTTPS_BODY_UNJUDGEABLE { 117 if NX_HTTPS_BODY_UNJUDGEABLE != NX_HTTPS_BODY_UNOBSERVED { 118 if NX_HTTPS_BODY_COMPLETE != NX_HTTPS_BODY_UNOBSERVED { distinct = 1 } } } } 119 gv_check("all-four-states-are-distinct-so-UNOBSERVED-cannot-collapse-into-COMPLETE" as *u8, distinct, ctr) 120 121 // ---------- the seal publishes BOTH numbers, and last-call-wins is the declared scope ---------- 122 _gc_body_seal(1000, 900) 123 var seal_ok: i64 = 0 124 if nx_https_last_body_state() == NX_HTTPS_BODY_TRUNCATED { 125 if nx_https_last_body_expected() == 1000 { 126 if nx_https_last_body_got() == 900 { seal_ok = 1 } } } 127 gv_check("the-seal-publishes-expected-AND-got-not-merely-a-flag" as *u8, seal_ok, ctr) 128 129 // THE LADDER HANDOFF, PROVEN: when the TLS-1.2 leg delivers, the ladder re-seals with 130 // body_target=0 so this axis stops talking about a body it did not read. Without this the static 131 // would still hold the rung-1 TRUNCATED verdict and report a healthy 1.2 body as cut. 132 _gc_body_seal(0, 50) 133 var handoff: i64 = 0 134 if nx_https_last_body_state() == NX_HTTPS_BODY_UNJUDGEABLE { 135 if nx_https_last_body_expected() == 0 { 136 if nx_https_last_body_got() == 50 { handoff = 1 } } } 137 gv_check("neg-control-a-later-seal-clears-an-earlier-TRUNCATED-verdict" as *u8, handoff, ctr) 138 139 // ---------- BIND THE PREDICATE TO THE ACTUAL WIRE FORMAT ---------- 140 // A producer and a consumer each correct in isolation can still disagree on the wire, so the 141 // arithmetic is re-derived here from real response BYTES through the transport own header 142 // parser, not from numbers this gate invented. 143 let buf: *u8 = sys_mmap(4096) 144 let whole: i64 = hg_resp(buf, "11" as *u8, 11, 0) 145 let tgt_w: i64 = hg_target(buf, whole) 146 var wire_w: i64 = 0 147 if tgt_w == whole { if nx_https_body_state(tgt_w, whole) == NX_HTTPS_BODY_COMPLETE { wire_w = 1 } } 148 gv_check("body-target-derived-from-a-real-response-lands-exactly-on-the-body-end" as *u8, wire_w, ctr) 149 150 // Same declared length, five body bytes delivered: the shortfall must be exactly six. 151 let cut: i64 = hg_resp(buf, "11" as *u8, 5, 0) 152 let tgt_c: i64 = hg_target(buf, cut) 153 var wire_c: i64 = 0 154 if nx_https_body_state(tgt_c, cut) == NX_HTTPS_BODY_TRUNCATED { 155 if nx_https_body_shortfall(tgt_c, cut) == 6 { wire_c = 1 } } 156 var wire_cg: i64 = 1 157 if wire_w == 1 { wire_cg = 0 } 158 gv_bite("a-real-response-cut-six-bytes-short-is-named-with-its-six" as *u8, wire_c, wire_cg, ctr) 159 160 // NEG-CONTROL ON THE HEADER MATCH ITSELF: X-Content-Length must not be read as Content-Length. 161 // Matching mid-line would invent a body_target out of an unrelated header and then report a 162 // perfectly complete body as cut -- a false positive manufactured by the parser. 163 let xonly: i64 = hg_resp(buf, "99999" as *u8, 20, 1) 164 let tgt_x: i64 = hg_target(buf, xonly) 165 var xok: i64 = 0 166 if tgt_x == 0 { if nx_https_body_state(tgt_x, xonly) == NX_HTTPS_BODY_UNJUDGEABLE { xok = 1 } } 167 gv_check("neg-control-X-Content-Length-is-not-mistaken-for-Content-Length" as *u8, xok, ctr) 168 169 gv_puts("\n observed: state(1000,900)=" as *u8); gv_num(nx_https_body_state(1000, 900)) 170 gv_puts(" state(1000,1000)=" as *u8); gv_num(nx_https_body_state(1000, 1000)) 171 gv_puts(" state(1000,1200)=" as *u8); gv_num(nx_https_body_state(1000, 1200)) 172 gv_puts(" state(0,5000)=" as *u8); gv_num(nx_https_body_state(0, 5000)) 173 gv_puts("\n shortfall(1000,900)=" as *u8); gv_num(nx_https_body_shortfall(1000, 900)) 174 gv_puts(" shortfall(1000,1000)=" as *u8); gv_num(nx_https_body_shortfall(1000, 1000)) 175 gv_puts("\n wire: whole_total=" as *u8); gv_num(whole) 176 gv_puts(" target=" as *u8); gv_num(tgt_w) 177 gv_puts(" | cut_total=" as *u8); gv_num(cut) 178 gv_puts(" target=" as *u8); gv_num(tgt_c) 179 gv_puts(" shortfall=" as *u8); gv_num(nx_https_body_shortfall(tgt_c, cut)) 180 gv_puts(" | xheader_target=" as *u8); gv_num(tgt_x) 181 gv_puts("\n" as *u8) 182 183 return gv_verdict("https_truncation" as *u8, ctr, "state codes 0=COMPLETE 1=TRUNCATED 2=UNJUDGEABLE 3=UNOBSERVED; fixtures are process-local byte buffers, rebuilt per tooth, shared with nothing" as *u8) 184}