nx_compare_gw_gate.nx source
↩ module page · 229 lines · 18116 B
1// nx_compare_gw_gate.nx -- GATE for the /compare gateway decision core (nx_compare_gw_lib).
2//
3// THE LOAD-BEARING TEETH ARE THE NO-LEAK ONES: a board a viewer may not see is NOT in the bytes of the
4// index rendered for that viewer (not-even-listed), and a request for it returns the SAME answer as a
5// request for a board that does not exist (NOTFOUND, never DENY, for an anonymous visitor). Everything
6// else checks a shape; those check the property the operator asked for -- the owner sees all, a client
7// sees a subset, and the subset boundary does not leak the existence of what is above it.
8//
9// Every function is exercised IN-PROCESS so a mutation bite reaches it; the conf and requests are built
10// IN MEMORY so no live file can change what this gate measures. The comment character in a fixture conf is
11// CONSTRUCTED (35) -- a literal hash inside a string is an nx_cc lexer trap.
12// license_tier: ORIGINAL No hw writes (Rule 26).
13import "nx_syscalls.nx"
14import "nx_compare_gw_lib.nx"
15import "nx_gate_verdict.nx"
16
17const CGG_BUF: i64 = 65536
18const CGG_SMALL: i64 = 256
19const CGG_WORD: i64 = 8
20
21func cgg_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
22func cgg_streq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 }
23// substring search over a NUL-terminated buffer
24func cgg_has(hay: *u8, needle: *u8) -> i64 {
25 let hn: i64 = cgg_slen(hay)
26 let m: i64 = cgg_slen(needle)
27 if m == 0 { return 1 }
28 var i: i64 = 0
29 while i + m <= hn {
30 var j: i64 = 0
31 var ok: i64 = 1
32 while j < m { if hay[i+j] != needle[j] { ok = 0; j = m } else { j = j + 1 } }
33 if ok == 1 { return 1 }
34 i = i + 1
35 }
36 return 0
37}
38
39func main() -> i64 {
40 let ctr: *i64 = gv_ctr()
41 gv_head("nx_compare_gw -- the /compare access wall: the owner sees all, a client sees a subset, and the subset does not leak" as *u8)
42
43 // ---- the fixture conf, built in memory (comment byte constructed) ----
44 // default public; two boards named: a public one and an owner-only one.
45 let conf: *u8 = sys_mmap(CGG_BUF)
46 var co: i64 = 0
47 conf[co] = 35 as u8; co = co + 1
48 co = cg_cat(conf, co, " fixture access.conf\ndefault|public\ndomain|folkgames|public\ndomain|synthroom|operator\ndomain|members|member\n" as *u8)
49 conf[co] = 0 as u8
50 let cn: i64 = co
51
52 // ---- cg_access_level: the four words and the fail-closed default ----
53 gv_check("T1 public reads level 0" as *u8, (cg_access_level("public" as *u8, 0, 6) == CG_LVL_PUBLIC) as i64, ctr)
54 gv_check("T2 viewer reads level 1" as *u8, (cg_access_level("viewer" as *u8, 0, 6) == CG_LVL_VIEWER) as i64, ctr)
55 gv_check("T3 operator reads level 3" as *u8, (cg_access_level("operator" as *u8, 0, 8) == CG_LVL_OPERATOR) as i64, ctr)
56 gv_check("T4 an unknown access word reads OPERATOR, fail-closed, never public" as *u8, (cg_access_level("nonsense" as *u8, 0, 8) == CG_LVL_OPERATOR) as i64, ctr)
57 gv_bite("neg-control-fail-closed a typo locks a board to the owner and does NOT read as public" as *u8, (cg_access_level("pubIic" as *u8, 0, 6) == CG_LVL_OPERATOR) as i64, (cg_access_level("public" as *u8, 0, 6) == CG_LVL_OPERATOR) as i64, ctr)
58
59 // ---- cg_required: a named board wins, else the default ----
60 gv_check("T5 a board declared public requires level 0" as *u8, (cg_required(conf, cn, "folkgames" as *u8) == CG_LVL_PUBLIC) as i64, ctr)
61 gv_check("T6 a board declared operator requires level 3" as *u8, (cg_required(conf, cn, "synthroom" as *u8) == CG_LVL_OPERATOR) as i64, ctr)
62 gv_check("T7 a board with NO row takes the declared default of public" as *u8, (cg_required(conf, cn, "typography" as *u8) == CG_LVL_PUBLIC) as i64, ctr)
63 // a conf with NO default row falls to operator -- names nothing, exposes nothing
64 let ndf: *u8 = sys_mmap(CGG_BUF)
65 var no: i64 = 0
66 no = cg_cat(ndf, no, "domain|folkgames|public\n" as *u8)
67 ndf[no] = 0 as u8
68 gv_check("T8 a conf that declares no default leaves an unnamed board at OPERATOR, fail-closed" as *u8, (cg_required(ndf, no, "typography" as *u8) == CG_LVL_OPERATOR) as i64, ctr)
69 gv_bite("neg-control-default-fail-closed an unnamed board is operator without a default row and public with one" as *u8, (cg_required(ndf, no, "typography" as *u8) == CG_LVL_OPERATOR) as i64, (cg_required(conf, cn, "typography" as *u8) == CG_LVL_OPERATOR) as i64, ctr)
70
71 // ---- cg_decide: the whole point ----
72 // owner (3) sees the operator board; a client (1) does not, and is told NOTFOUND, not DENY; an
73 // anonymous visitor (0) is told NOTFOUND for it too; but a signed-in client asking for a MEMBER board
74 // (needs 2) is told DENY because their existence as a viewer is already known.
75 gv_check("T9 owner level 3 SERVES an operator board" as *u8, (cg_decide(3, CG_LVL_OPERATOR, 1) == CG_D_SERVE) as i64, ctr)
76 gv_check("T10 a client level 1 gets NOTFOUND for an operator board, never DENY -- existence not leaked" as *u8, (cg_decide(1, CG_LVL_OPERATOR, 1) == CG_D_NOTFOUND) as i64, ctr)
77 gv_check("T11 an anonymous level 0 gets NOTFOUND for a gated board" as *u8, (cg_decide(0, CG_LVL_VIEWER, 1) == CG_D_NOTFOUND) as i64, ctr)
78 gv_check("T12 a public board is SERVED to everyone including anonymous" as *u8, (cg_decide(0, CG_LVL_PUBLIC, 1) == CG_D_SERVE) as i64, ctr)
79 gv_check("T13 a board that does not exist is NOTFOUND regardless of level" as *u8, (cg_decide(3, CG_LVL_PUBLIC, 0) == CG_D_NOTFOUND) as i64, ctr)
80 // THE NO-LEAK DISCRIMINATOR: for the SAME operator board, an owner SERVES and a client gets the exact
81 // answer a missing board gives. A leaky gate would answer DENY, which confirms the board is there.
82 gv_check("T14 the client's answer for an operator board EQUALS the answer for a missing board" as *u8, (cg_decide(1, CG_LVL_OPERATOR, 1) == cg_decide(1, CG_LVL_PUBLIC, 0)) as i64, ctr)
83 gv_bite("neg-control-no-leak an anonymous client cannot distinguish a hidden board from an absent one, but the owner can serve it" as *u8, (cg_decide(0, CG_LVL_OPERATOR, 1) == CG_D_NOTFOUND) as i64, (cg_decide(3, CG_LVL_OPERATOR, 1) == CG_D_NOTFOUND) as i64, ctr)
84
85 // ---- cg_split: the request path under /compare ----
86 let dom: *u8 = sys_mmap(CGG_SMALL)
87 let rest: *u8 = sys_mmap(CGG_BUF)
88 let s_idx: i64 = cg_split("/compare" as *u8, 8, dom, CGG_SMALL, rest, CGG_BUF)
89 gv_check("T15 /compare is the index" as *u8, (s_idx == 1) as i64, ctr)
90 let s_idx2: i64 = cg_split("/compare/" as *u8, 9, dom, CGG_SMALL, rest, CGG_BUF)
91 gv_check("T16 /compare/ is also the index" as *u8, (s_idx2 == 1) as i64, ctr)
92 let s_dom: i64 = cg_split("/compare/synthroom" as *u8, 18, dom, CGG_SMALL, rest, CGG_BUF)
93 gv_check("T17 /compare/synthroom names the board and no rest" as *u8, (s_dom == 0) as i64 & cgg_streq(dom, "synthroom" as *u8) & (rest[0] == (0 as u8)) as i64, ctr)
94 let s_file: i64 = cg_split("/compare/synthroom/api.json" as *u8, 27, dom, CGG_SMALL, rest, CGG_BUF)
95 gv_check("T18 /compare/synthroom/api.json names the board and the rest" as *u8, (s_file == 0) as i64 & cgg_streq(dom, "synthroom" as *u8) & cgg_streq(rest, "api.json" as *u8), ctr)
96 let s_bad: i64 = cg_split("/gallery/x" as *u8, 10, dom, CGG_SMALL, rest, CGG_BUF)
97 gv_check("T19 a path outside /compare is refused, not guessed" as *u8, (s_bad == (0 - 1)) as i64, ctr)
98
99 // ---- cg_path_safe: boundary defence ----
100 gv_check("T20 a plain relative file is safe" as *u8, cg_path_safe("api.json" as *u8, 8), ctr)
101 gv_check("T21 a climbing path is refused" as *u8, (cg_path_safe("../secret" as *u8, 9) == 0) as i64, ctr)
102 gv_check("T22 an absolute path is refused" as *u8, (cg_path_safe("/etc/passwd" as *u8, 11) == 0) as i64, ctr)
103 gv_bite("neg-control-path-safe the traversal check fires on .. and not on a clean name" as *u8, (cg_path_safe("a/../b" as *u8, 6) == 0) as i64, (cg_path_safe("a/b" as *u8, 3) == 0) as i64, ctr)
104
105 // ---- cg_declared: the conf enumerates exactly its domain rows ----
106 let names: *u8 = sys_mmap(CGG_BUF)
107 let dc: *i64 = sys_mmap(CGG_WORD) as *i64
108 cg_declared(conf, cn, names, CGG_BUF, dc)
109 gv_check("T23 the conf declares exactly its three domain rows" as *u8, (dc[0] == 3) as i64, ctr)
110
111 // ---- the INDEX no-leak, measured on the BYTES ----
112 // Build the owner's index (all three boards) and the client's index (only the public ones), then prove
113 // the operator board's name is present for the owner and ABSENT from the client's bytes.
114 let cand: *i64 = sys_mmap(4 * CGG_WORD) as *i64
115 cand[0] = ("folkgames" as *u8) as i64
116 cand[1] = ("synthroom" as *u8) as i64
117 cand[2] = ("members" as *u8) as i64
118 let owner_ix: *u8 = sys_mmap(CGG_BUF)
119 var oo: i64 = cg_index_open(owner_ix, CG_LVL_OPERATOR, 0)
120 var oshown: i64 = 0
121 var ci: i64 = 0
122 while ci < 3 {
123 let d: *u8 = cand[ci] as *u8
124 if CG_LVL_OPERATOR >= cg_required(conf, cn, d) { oo = cg_index_row(owner_ix, oo, d, "" as *u8); oshown = oshown + 1 }
125 ci = ci + 1
126 }
127 cg_index_close(owner_ix, oo, oshown)
128 let client_ix: *u8 = sys_mmap(CGG_BUF)
129 var xo: i64 = cg_index_open(client_ix, CG_LVL_VIEWER, 0)
130 var xshown: i64 = 0
131 ci = 0
132 while ci < 3 {
133 let d: *u8 = cand[ci] as *u8
134 if CG_LVL_VIEWER >= cg_required(conf, cn, d) { xo = cg_index_row(client_ix, xo, d, "" as *u8); xshown = xshown + 1 }
135 ci = ci + 1
136 }
137 cg_index_close(client_ix, xo, xshown)
138 gv_check("T24 the owner index shows all three boards" as *u8, (oshown == 3) as i64, ctr)
139 gv_check("T25 the client index shows ONLY the one public board" as *u8, (xshown == 1) as i64, ctr)
140 gv_check("T26 the operator board name IS in the owner bytes" as *u8, cgg_has(owner_ix, "synthroom" as *u8), ctr)
141 gv_check("T27 NOT-EVEN-LISTED: the operator board name is ABSENT from the client bytes" as *u8, (cgg_has(client_ix, "synthroom" as *u8) == 0) as i64, ctr)
142 gv_check("T28 the member board is likewise ABSENT from the level-1 client bytes" as *u8, (cgg_has(client_ix, "members" as *u8) == 0) as i64, ctr)
143 gv_check("T29 the public board IS in the client bytes" as *u8, cgg_has(client_ix, "folkgames" as *u8), ctr)
144 gv_check("T39 the served index head advertises the daily feed (rel=alternate, feed.xml) at every level" as *u8, cgg_has(owner_ix, "rel='alternate'" as *u8) & cgg_has(owner_ix, "href='/compare/feed.xml'" as *u8) & cgg_has(client_ix, "href='/compare/feed.xml'" as *u8), ctr)
145 gv_bite("neg-control-index-leak the hidden board appears in the owner index and not in the client index" as *u8, cgg_has(owner_ix, "synthroom" as *u8), cgg_has(client_ix, "synthroom" as *u8), ctr)
146
147 // ---- request parsers, in memory ----
148 let path: *u8 = sys_mmap(CGG_SMALL)
149 let pl: i64 = cg_req_path("GET /compare/synthroom?x=1 HTTP/1.1\r\n" as *u8, 37, path, CGG_SMALL)
150 gv_check("T30 the request path is taken up to the query string" as *u8, cgg_streq(path, "/compare/synthroom" as *u8), ctr)
151 let hv: *u8 = sys_mmap(CGG_SMALL)
152 let hl: i64 = cg_req_header("GET / HTTP/1.1\r\nX-Nishi-Session: abc123\r\n\r\n" as *u8, 43, "X-Nishi-Session:" as *u8, hv, CGG_SMALL)
153 gv_check("T31 the session header value is extracted, trimmed of leading space and CR" as *u8, cgg_streq(hv, "abc123" as *u8), ctr)
154
155 // ---- EC47: the FEED no-leak, measured on the BYTES the projector emits ----
156 let feed: *u8 = sys_mmap(CGG_BUF)
157 var fo: i64 = 0
158 fo = cg_cat(feed, fo, "<?xml version='1.0' encoding='UTF-8'?>\n<rss version='2.0'><channel><title>fixture</title>\n" as *u8)
159 fo = cg_cat(feed, fo, "<item><title>folkgames 2026-09-16</title><link>https://nishifamily.com/compare/folkgames/</link><guid>a</guid></item>\n" as *u8)
160 fo = cg_cat(feed, fo, "<item><title>synthroom 2026-09-16</title><link>https://nishifamily.com/compare/synthroom/</link><guid>b</guid></item>\n" as *u8)
161 fo = cg_cat(feed, fo, "<item><title>members 2026-09-16</title><link>https://nishifamily.com/compare/members/</link><guid>c</guid></item>\n" as *u8)
162 fo = cg_cat(feed, fo, "</channel></rss>\n" as *u8)
163 feed[fo] = 0 as u8
164 let fdom: *u8 = sys_mmap(CGG_SMALL)
165 let fctr: *i64 = sys_mmap(2 * CGG_WORD) as *i64
166 let fdrop: *i64 = ((fctr as i64) + CGG_WORD) as *i64
167 let fowner: *u8 = sys_mmap(CGG_BUF)
168 let fol: i64 = cg_feed_project(feed, fo, CG_LVL_OPERATOR, conf, cn, fowner, CGG_BUF, fdom, CGG_SMALL, fctr, fdrop)
169 if fol >= 0 { fowner[fol] = 0 as u8 }
170 gv_check("T32 the owner's feed keeps all three items and is byte-for-byte the whole fixture" as *u8, (fol == fo) as i64 & (fctr[0] == 3) as i64 & (fdrop[0] == 0) as i64 & cgg_streq(fowner, feed), ctr)
171 let fclient: *u8 = sys_mmap(CGG_BUF)
172 let fcl: i64 = cg_feed_project(feed, fo, CG_LVL_VIEWER, conf, cn, fclient, CGG_BUF, fdom, CGG_SMALL, fctr, fdrop)
173 if fcl >= 0 { fclient[fcl] = 0 as u8 }
174 gv_check("T33 the client's feed keeps exactly the one public item and drops two" as *u8, (fcl > 0) as i64 & (fcl < fo) as i64 & (fctr[0] == 1) as i64 & (fdrop[0] == 2) as i64, ctr)
175 gv_check("T34 NOT-EVEN-LISTED in the feed: synthroom and members are ABSENT from the client bytes, folkgames present" as *u8, cgg_has(fclient, "folkgames" as *u8) & (cgg_has(fclient, "synthroom" as *u8) == 0) as i64 & (cgg_has(fclient, "members" as *u8) == 0) as i64, ctr)
176 gv_check("T35 the client's feed is still one whole document: xml head kept, channel and rss closed" as *u8, cgg_has(fclient, "<?xml version" as *u8) & cgg_has(fclient, "</channel></rss>" as *u8), ctr)
177 let fanon: *u8 = sys_mmap(CGG_BUF)
178 let fan: i64 = cg_feed_project(feed, fo, CG_LVL_PUBLIC, conf, cn, fanon, CGG_BUF, fdom, CGG_SMALL, fctr, fdrop)
179 if fan >= 0 { fanon[fan] = 0 as u8 }
180 gv_check("T36 an anonymous visitor's feed equals the client's here (both see only the public board)" as *u8, (fan == fcl) as i64 & cgg_streq(fanon, fclient), ctr)
181 // an item with no board link is served to NOBODY, the owner included -- an unattributable row is not a public row
182 let feed2: *u8 = sys_mmap(CGG_BUF)
183 var f2: i64 = 0
184 f2 = cg_cat(feed2, f2, "<rss><channel>\n<item><title>no link</title><guid>d</guid></item>\n</channel></rss>\n" as *u8)
185 feed2[f2] = 0 as u8
186 let fout2: *u8 = sys_mmap(CGG_BUF)
187 let f2l: i64 = cg_feed_project(feed2, f2, CG_LVL_OPERATOR, conf, cn, fout2, CGG_BUF, fdom, CGG_SMALL, fctr, fdrop)
188 if f2l >= 0 { fout2[f2l] = 0 as u8 }
189 gv_check("neg-control-unattributable-item an item without a board link is dropped even for the owner, and the document stays closed" as *u8, (f2l > 0) as i64 & (fctr[0] == 0) as i64 & (fdrop[0] == 1) as i64 & (cgg_has(fout2, "no link" as *u8) == 0) as i64 & cgg_has(fout2, "</channel></rss>" as *u8), ctr)
190 // a truncated feed (an item that never closes) is REFUSED, so the daemon can never serve half a feed
191 let feed3: *u8 = sys_mmap(CGG_BUF)
192 var f3: i64 = 0
193 f3 = cg_cat(feed3, f3, "<rss><channel>\n<item><title>open</title><link>https://nishifamily.com/compare/folkgames/</link>\n" as *u8)
194 feed3[f3] = 0 as u8
195 gv_check("T37 an item without its close is REFUSED (-1), never projected" as *u8, (cg_feed_project(feed3, f3, CG_LVL_OPERATOR, conf, cn, fout2, CGG_BUF, fdom, CGG_SMALL, fctr, fdrop) == (0 - 1)) as i64, ctr)
196 // a feed with no items is served whole at every level
197 let feed4: *u8 = sys_mmap(CGG_BUF)
198 var f4: i64 = 0
199 f4 = cg_cat(feed4, f4, "<?xml version='1.0'?>\n<rss><channel><title>empty</title></channel></rss>\n" as *u8)
200 feed4[f4] = 0 as u8
201 let f4l: i64 = cg_feed_project(feed4, f4, CG_LVL_PUBLIC, conf, cn, fout2, CGG_BUF, fdom, CGG_SMALL, fctr, fdrop)
202 if f4l >= 0 { fout2[f4l] = 0 as u8 }
203 gv_check("T38 a feed with no items projects to identical bytes for an anonymous visitor" as *u8, (f4l == f4) as i64 & cgg_streq(fout2, feed4) & (fctr[0] == 0) as i64 & (fdrop[0] == 0) as i64, ctr)
204 // ---- the file served under a board: a directory is its index.html, a full read is refused (2026-09-16) ----
205 let bf: *u8 = sys_mmap(CGG_SMALL)
206 cg_board_file("" as *u8, 0, bf)
207 gv_check("T40 an empty rest serves the board's own index.html" as *u8, cgg_streq(bf, "index.html" as *u8), ctr)
208 cg_board_file("frontier" as *u8, 1, bf)
209 gv_check("T41 a rest that names a directory serves that directory's index.html (the radar at <board>/frontier)" as *u8, cgg_streq(bf, "frontier/index.html" as *u8), ctr)
210 cg_board_file("frontier/" as *u8, 1, bf)
211 gv_check("T42 a trailing slash on a directory does not double the slash" as *u8, cgg_streq(bf, "frontier/index.html" as *u8), ctr)
212 cg_board_file("api.json" as *u8, 0, bf)
213 gv_check("T43 a rest that names a file is served as itself" as *u8, cgg_streq(bf, "api.json" as *u8), ctr)
214 let bf2: *u8 = sys_mmap(CGG_SMALL)
215 cg_board_file("frontier" as *u8, 1, bf2)
216 let dir_as_self: i64 = cgg_streq(bf2, "frontier" as *u8)
217 cg_board_file("frontier" as *u8, 0, bf2)
218 gv_bite("neg-control-directory-never-read-as-a-file a directory rest is rewritten to its index while the same name as a file is not" as *u8, (dir_as_self == 0) as i64, (cgg_streq(bf2, "frontier" as *u8) == 0) as i64, ctr)
219 gv_check("T44 a read that filled the body reserve is REFUSED, never served truncated" as *u8, (cg_body_verdict(CGG_BUF, CGG_BUF) == CG_BODY_TOO_LARGE) as i64 & (cg_body_verdict(CGG_BUF + 1, CGG_BUF) == CG_BODY_TOO_LARGE) as i64, ctr)
220 gv_check("T45 a read under the reserve is served, including an empty file, and an unreadable one is missing" as *u8, (cg_body_verdict(CGG_BUF - 1, CGG_BUF) == CG_BODY_SERVE) as i64 & (cg_body_verdict(0, CGG_BUF) == CG_BODY_SERVE) as i64 & (cg_body_verdict(0 - 1, CGG_BUF) == CG_BODY_MISSING) as i64, ctr)
221 gv_kv("feed_fixture_bytes" as *u8, fo)
222 gv_kv("feed_owner_bytes" as *u8, fol)
223 gv_kv("feed_client_bytes" as *u8, fcl)
224
225
226 let rc: i64 = gv_verdict("COMPARE-GW" as *u8, ctr, "the owner sees all, a client sees a subset, and a hidden board is indistinguishable from an absent one" as *u8)
227 sys_exit(rc)
228 return rc
229}