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}