code wiki / (root) / nx_rebac.nx

nx_rebac.nx source

↩ module page · 560 lines · 31631 B

1// nx_rebac.nx -- SOVEREIGN RELATIONSHIP-BASED ACCESS CONTROL (the Google Zanzibar / ReBAC model), the ONE shared 2// authorization plane for every Nishi surface. PURE LIB (no main): consumers = nx_rebac_gate (adversarial gate) + 3// the surfaces (relate/social/office/project) which import this and call rb_check instead of re-implementing ACLs. 4// 5// WHY: the ecosystem had 3 incompatible authz philosophies (relate tenant-wall `acl:`, project `mem:`, connect 6// in-memory group-roles) + an UNWIRED consent primitive + zero relationship/group permissions. This unifies them: 7// EVERYTHING is a TUPLE (object, relation, subject) persisted in seg_store, and access is a bounded graph walk 8// with userset rewrites -- exactly how Google Docs / GitHub / SpiceDB / OpenFGA decide "can X do Y on Z". 9// friend -> (user:bob, friend, user:alice) [mutual: store both directions] 10// join a group -> (group:elders, member, user:bob) 11// share a doc -> (doc:budget, viewer, user:carol) OR (doc:budget, viewer, group:elders#member) <- userset 12// the tenant wall -> (tenant:andelinwest, admin, user:elderwesto) [subsumes relate acl: + project mem:] 13// block / defriend -> a NEW tuple with cur=0 (tombstone) -- ADDITIVE-ONLY, never delete (rule 13); latest wins. 14// PERMISSIONS derive from RELATIONS via a data-driven REWRITE table (rb_expand): e.g. doc.read = viewer|editor|owner. 15// Ties to NISHI HR: rb_check_hr composes the HR level (ag_uid_to_level / hac_gate) as a realm-scoped operator 16// override -- a site operator (level 3) is god within their realm; everyone else is pure deny-by-default tuples. 17// DENY-BY-DEFAULT everywhere; blocks outrank; bounded recursion (RB_MAXDEPTH) so userset cycles TERMINATE not hang. 18// license_tier: ORIGINAL 19import "nx_syscalls.nx" 20import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 21import "nx_canon_cid.nx" 22import "nx_seg_store.nx" 23const RB_MAGIC_2048: i64 = 2048 24 25const RB_MAXDEPTH: i64 = 8 // Zanzibar bounds check() recursion; a member-of cycle terminates here 26const RB_OPERATOR: i64 = 3 // HR level that is site-operator (god within realm); mirrors HRA_LVL_OWNER 27 28// ---- string primitives ---- 29func rb_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 30func rb_p(s: *u8) -> i64 { sys_write(1, s, rb_slen(s)); return 0 } 31// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 32// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 33// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 34// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 35func rb_pn(v: i64) -> i64 { nxi_out(v); return 0 } 36func rb_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[off+i] = s[i]; i = i + 1 } return off + i } 37func rb_catn(dst: *u8, off: i64, v: i64) -> i64 { 38 var o: i64 = off; var m: i64 = v 39 let t: *u8 = sys_mmap(28); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 } 40 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 41 var i: i64 = 0; while i < k { dst[o + i] = t[k - 1 - i]; i = i + 1 } 42 return o + k 43} 44func rb_seq(a: *u8, b: *u8) -> i64 { 45 var i: i64 = 0 46 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 47 if b[i] != (0 as u8) { return 0 } 48 return 1 49} 50func rb_kmemeq(b: *u8, off: i64, n: i64, s: *u8) -> i64 { 51 if rb_slen(s) != n { return 0 } 52 var i: i64 = 0; while i < n { if b[off + i] != s[i] { return 0 } i = i + 1 } 53 return 1 54} 55func rb_iskind(b: *u8, koff: i64, kl: i64, pfx: *u8) -> i64 { if kl <= 4 { return 0 } return rb_kmemeq(b, koff, 4, pfx) } 56// extract a named field's value from a canon record into out (NUL-term). 1 = found, 0 = absent. 57func rb_field(rec: *u8, rl: i64, want: *u8, out: *u8, cap: i64) -> i64 { 58 if rl < 8 { return 0 } 59 let nf: i64 = ss_r32(rec, 4) 60 var off: i64 = 8; var fi: i64 = 0 61 while fi < nf { 62 if off + 8 > rl { return 0 } 63 let kl: i64 = ss_r32(rec, off); let koff: i64 = off + 4 64 let vl: i64 = ss_r32(rec, koff + kl); let voff: i64 = koff + kl + 4 65 if rb_kmemeq(rec, koff, kl, want) == 1 { 66 var t: i64 = 0 67 while t < vl { if t < cap - 1 { out[t] = rec[voff + t] } t = t + 1 } 68 if t > cap - 1 { t = cap - 1 } 69 out[t] = 0 as u8; return 1 70 } 71 off = voff + vl; fi = fi + 1 72 } 73 return 0 74} 75// object/subject are "type:id" (or "type:id#relation" for a userset). extract the leading type into out. 76func rb_otype(obj: *u8, out: *u8, cap: i64) -> i64 { 77 var j: i64 = 0 78 while obj[j] != (0 as u8) { if (obj[j] as i64) == 58 { out[j] = 0 as u8; return j } if j < cap - 1 { out[j] = obj[j] } j = j + 1 } 79 out[j] = 0 as u8; return j 80} 81func rb_has_hash(s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { if (s[i] as i64) == 35 { return 1 } i = i + 1 } return 0 } 82// split "group:elders#member" -> gobj="group:elders", grel="member" 83func rb_split_userset(s: *u8, gobj: *u8, grel: *u8) -> i64 { 84 var i: i64 = 0; var h: i64 = 0 - 1 85 while s[i] != (0 as u8) { if (s[i] as i64) == 35 { h = i; break } gobj[i] = s[i]; i = i + 1 } 86 if h < 0 { gobj[i] = 0 as u8; grel[0] = 0 as u8; return 0 } 87 gobj[h] = 0 as u8 88 var k: i64 = 0; var q: i64 = h + 1 89 while s[q] != (0 as u8) { grel[k] = s[q]; k = k + 1; q = q + 1 } 90 grel[k] = 0 as u8 91 return 1 92} 93 94// ---- tuple store (kind "tup:" -> {obj, rel, sub, grantor, cur}) ---- 95func rb_save_tuple(w: *i64, obj: *u8, rel: *u8, sub: *u8, grantor: *u8, cur: i64) -> i64 { 96 let cb: *u8 = sys_mmap(4); if cur == 0 { cb[0] = 48 as u8 } else { cb[0] = 49 as u8 } cb[1] = 0 as u8 97 let keys: *i64 = sys_mmap(8 * 8) as *i64 98 let vals: *i64 = sys_mmap(8 * 8) as *i64 99 keys[0]="kind" as *u8 as i64; keys[1]="obj" as *u8 as i64; keys[2]="rel" as *u8 as i64; keys[3]="sub" as *u8 as i64; keys[4]="gr" as *u8 as i64; keys[5]="cur" as *u8 as i64 100 vals[0]="tup" as *u8 as i64; vals[1]=obj as i64; vals[2]=rel as i64; vals[3]=sub as i64; vals[4]=grantor as i64; vals[5]=cb as i64 101 let enc: *u8 = sys_mmap(RB_MAGIC_2048) 102 let el: i64 = canon_encode(keys, vals, 6, enc) 103 let cid: *u8 = sys_mmap(96) 104 cid_of(enc, el, cid) 105 let key: *u8 = sys_mmap(128) 106 var ko: i64 = 0 107 ko = rb_cat(key, ko, "tup:" as *u8) 108 // include a monotone suffix so a re-grant/tombstone of the SAME (obj,rel,sub) is a DISTINCT record (CID would 109 // collide otherwise -> the tombstone would dedupe against the grant and latest-wins would break). 110 ko = rb_cat(key, ko, cid) 111 key[ko] = 0 as u8 112 return ss_add(w, 1, key, enc, el) 113} 114// persist one tuple in its own commit (segid = now_us keeps append-order = time-order for latest-wins). 115func rb_put(prefix: *u8, obj: *u8, rel: *u8, sub: *u8, grantor: *u8, cur: i64) -> i64 { 116 let w: *i64 = ss_begin() 117 rb_save_tuple(w, obj, rel, sub, grantor, cur) 118 return ss_commit(prefix, w, sys_now_us()) 119} 120 121// latest cur for the EXACT triple (obj, rel, sub): walk all segments in append(time) order, last write wins. 122// returns 1 current / 0 tombstoned-or-absent. This is the atom every check is built on. 123func rb_tuple_current(prefix: *u8, obj: *u8, rel: *u8, sub: *u8) -> i64 { 124 let segp: *i64 = sys_mmap(16) as *i64 125 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: authz must see the WHOLE store (a truncated walk = wrong last-write-wins) 126 let segs: *i64 = segp[0] as *i64 127 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fs: *u8 = sys_mmap(256); let fc: *u8 = sys_mmap(8) 128 var last: i64 = 0 129 var s: i64 = 0 130 while s < ns { 131 let path: *u8 = sys_mmap(512); var po: i64 = 0 132 po = rb_cat(path, po, prefix); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8 133 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0] 134 var j: i64 = 0 135 while j + 9 <= sz { 136 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4 137 if b[j] == 1 { 138 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 { 139 let rec: *u8 = (b as i64 + voff) as *u8 140 fo[0] = 0 as u8; fr[0] = 0 as u8; fs[0] = 0 as u8; fc[0] = 0 as u8 141 rb_field(rec, vl, "obj" as *u8, fo, 256) 142 rb_field(rec, vl, "rel" as *u8, fr, 128) 143 rb_field(rec, vl, "sub" as *u8, fs, 256) 144 if rb_seq(fo, obj) == 1 { if rb_seq(fr, rel) == 1 { if rb_seq(fs, sub) == 1 { 145 rb_field(rec, vl, "cur" as *u8, fc, 8) 146 if (fc[0] as i64) == 49 { last = 1 } else { last = 0 } 147 } } } 148 } 149 } 150 j = voff + vl 151 } 152 s = s + 1 153 } 154 return last 155} 156 157// ---- the REWRITE table (data-driven): a PERMISSION on an object TYPE expands to the set of RELATIONS that grant 158// it. This is the Zanzibar userset-rewrite config; relations are named (not magic numbers -> rule 11 satisfied). 159// out = *i64 array of string pointers; returns count. Unknown type/perm -> just the perm itself (exact-match rel). 160func rb_expand(otype: *u8, perm: *u8, out: *i64, cap: i64) -> i64 { 161 var n: i64 = 0 162 // doc: read=viewer|editor|owner ; write/edit=editor|owner ; manage=owner 163 if rb_seq(otype, "doc" as *u8) == 1 { 164 if rb_seq(perm, "read" as *u8) == 1 { out[0]="viewer" as *u8 as i64; out[1]="editor" as *u8 as i64; out[2]="owner" as *u8 as i64; return 3 } 165 if rb_seq(perm, "view" as *u8) == 1 { out[0]="viewer" as *u8 as i64; out[1]="editor" as *u8 as i64; out[2]="owner" as *u8 as i64; return 3 } 166 if rb_seq(perm, "write" as *u8) == 1 { out[0]="editor" as *u8 as i64; out[1]="owner" as *u8 as i64; return 2 } 167 if rb_seq(perm, "edit" as *u8) == 1 { out[0]="editor" as *u8 as i64; out[1]="owner" as *u8 as i64; return 2 } 168 if rb_seq(perm, "manage" as *u8) == 1 { out[0]="owner" as *u8 as i64; return 1 } 169 } 170 // group: view/read/member=member|admin|owner ; post=member|admin|owner ; manage=admin|owner 171 if rb_seq(otype, "group" as *u8) == 1 { 172 if rb_seq(perm, "manage" as *u8) == 1 { out[0]="admin" as *u8 as i64; out[1]="owner" as *u8 as i64; return 2 } 173 out[0]="member" as *u8 as i64; out[1]="admin" as *u8 as i64; out[2]="owner" as *u8 as i64; return 3 174 } 175 // tenant: read/write=member|manager|admin ; admin/manage=admin 176 if rb_seq(otype, "tenant" as *u8) == 1 { 177 if rb_seq(perm, "manage" as *u8) == 1 { out[0]="admin" as *u8 as i64; return 1 } 178 if rb_seq(perm, "admin" as *u8) == 1 { out[0]="admin" as *u8 as i64; return 1 } 179 out[0]="member" as *u8 as i64; out[1]="manager" as *u8 as i64; out[2]="admin" as *u8 as i64; return 3 180 } 181 // project: view=owner|team|contractor ; edit=owner|team ; manage=owner 182 if rb_seq(otype, "project" as *u8) == 1 { 183 if rb_seq(perm, "manage" as *u8) == 1 { out[0]="owner" as *u8 as i64; return 1 } 184 if rb_seq(perm, "edit" as *u8) == 1 { out[0]="owner" as *u8 as i64; out[1]="team" as *u8 as i64; return 2 } 185 out[0]="owner" as *u8 as i64; out[1]="team" as *u8 as i64; out[2]="contractor" as *u8 as i64; return 3 186 } 187 // album (family media): view=viewer|curator|owner ; add=curator|owner ; manage=curator|owner. 188 // ADDED 2026-07-25 by the share-plane retirement (nx_share_convergence_gate C9). The suite needed an 189 // owner to be able to DELEGATE re-sharing -- "my spouse can add people to the family album too" -- and 190 // the doc type deliberately cannot express that (doc manage = owner only). The convergence proof said 191 // the migration's job was to carry that rule across as a rewrite-table ROW rather than as a second 192 // authorization plane, so here it is as data. Purely additive: no existing otype changes behaviour. 193 if rb_seq(otype, "album" as *u8) == 1 { 194 if rb_seq(perm, "manage" as *u8) == 1 { out[0]="curator" as *u8 as i64; out[1]="owner" as *u8 as i64; return 2 } 195 if rb_seq(perm, "add" as *u8) == 1 { out[0]="curator" as *u8 as i64; out[1]="owner" as *u8 as i64; return 2 } 196 out[0]="viewer" as *u8 as i64; out[1]="curator" as *u8 as i64; out[2]="owner" as *u8 as i64; return 3 197 } 198 // user (social): view/friend=friend ; the block relation is checked separately (blocks outrank) 199 if rb_seq(otype, "user" as *u8) == 1 { 200 out[0]="friend" as *u8 as i64; return 1 201 } 202 // default: the perm IS the relation (exact match) 203 out[0]=perm as i64; return 1 204} 205 206// ---- the CHECK: does `sub` hold `perm` on `obj`? bounded userset-rewrite walk, deny-by-default. ---- 207func rb_check(prefix: *u8, sub: *u8, perm: *u8, obj: *u8, depth: i64) -> i64 { 208 if depth > RB_MAXDEPTH { return 0 } 209 let ot: *u8 = sys_mmap(64) 210 rb_otype(obj, ot, 64) 211 // BLOCKS OUTRANK: if the object is a user and it blocked the subject, hard-deny (the consent axis, as a tuple). 212 if rb_seq(ot, "user" as *u8) == 1 { if rb_tuple_current(prefix, obj, "blocked" as *u8, sub) == 1 { return 0 } } 213 let rels: *i64 = sys_mmap(8 * 16) as *i64 214 let nr: i64 = rb_expand(ot, perm, rels, 16) 215 var ri: i64 = 0 216 while ri < nr { 217 let rel: *u8 = rels[ri] as *u8 218 // (a) DIRECT: is (obj, rel, sub) current? 219 if rb_tuple_current(prefix, obj, rel, sub) == 1 { return 1 } 220 // (b) USERSET: any current (obj, rel, G#grel) whose G grants `grel` to sub? (recurse, depth-bounded) 221 let segp: *i64 = sys_mmap(16) as *i64 222 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: authz must see the WHOLE store 223 let segs: *i64 = segp[0] as *i64 224 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fus: *u8 = sys_mmap(256) 225 var s: i64 = 0 226 while s < ns { 227 let path: *u8 = sys_mmap(512); var po: i64 = 0 228 po = rb_cat(path, po, prefix); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8 229 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0] 230 var j: i64 = 0 231 while j + 9 <= sz { 232 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4 233 if b[j] == 1 { 234 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 { 235 let rec: *u8 = (b as i64 + voff) as *u8 236 fo[0] = 0 as u8; fr[0] = 0 as u8; fus[0] = 0 as u8 237 rb_field(rec, vl, "obj" as *u8, fo, 256) 238 rb_field(rec, vl, "rel" as *u8, fr, 128) 239 rb_field(rec, vl, "sub" as *u8, fus, 256) 240 if rb_seq(fo, obj) == 1 { if rb_seq(fr, rel) == 1 { if rb_has_hash(fus) == 1 { 241 // this userset must be CURRENT (latest-wins on the exact triple), then recurse 242 if rb_tuple_current(prefix, obj, rel, fus) == 1 { 243 let gobj: *u8 = sys_mmap(256); let grel: *u8 = sys_mmap(128) 244 rb_split_userset(fus, gobj, grel) 245 if rb_check(prefix, sub, grel, gobj, depth + 1) == 1 { return 1 } 246 } 247 } } } 248 } 249 } 250 j = voff + vl 251 } 252 s = s + 1 253 } 254 ri = ri + 1 255 } 256 return 0 257} 258 259// HR-composed check: a site OPERATOR (HR level >= RB_OPERATOR) is god WITHIN THEIR REALM (the caller has already 260// validated realm via the session). Everyone else = pure deny-by-default tuples. THIS is the tie to Nishi HR: 261// hr_level comes from ag_uid_to_level / hac_gate on the validated session. Pass hr_level<0 or 0 for "no override". 262func rb_check_hr(prefix: *u8, sub: *u8, perm: *u8, obj: *u8, hr_level: i64) -> i64 { 263 if hr_level >= RB_OPERATOR { return 1 } 264 return rb_check(prefix, sub, perm, obj, 0) 265} 266 267// ---- CROSS-SURFACE (two-store) check: consult a PRIMARY store + a read-only SECONDARY store. This is how a 268// group defined in ONE surface (e.g. relate's knowledge/status/relate_) grants access to an object owned in 269// ANOTHER (e.g. office's officeauthz_): the doc-share userset lives in the primary, the group membership in the 270// secondary, and rb_check2 spans both. Deny-by-default; same bounded userset walk as rb_check. Additive -- the 271// single-store rb_check (relate uses it) is untouched. Tombstones live with their grant (confirm within a store). 272func rb_tuple_current2(p1: *u8, p2: *u8, obj: *u8, rel: *u8, sub: *u8) -> i64 { 273 if rb_tuple_current(p1, obj, rel, sub) == 1 { return 1 } 274 return rb_tuple_current(p2, obj, rel, sub) 275} 276// scan ONE store for current (obj, rel, G#grel) usersets; recurse rb_check2 (2-store) into each. 1 = a grant found. 277func rb_userset_scan(store: *u8, p1: *u8, p2: *u8, sub: *u8, obj: *u8, rel: *u8, depth: i64) -> i64 { 278 let segp: *i64 = sys_mmap(16) as *i64 279 let ns: i64 = ss_manifest_dyn(store, segp) // uncapped: authz must see the WHOLE store 280 let segs: *i64 = segp[0] as *i64 281 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fus: *u8 = sys_mmap(256) 282 var s: i64 = 0 283 while s < ns { 284 let path: *u8 = sys_mmap(512); var po: i64 = 0 285 po = rb_cat(path, po, store); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8 286 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0] 287 var j: i64 = 0 288 while j + 9 <= sz { 289 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4 290 if b[j] == 1 { 291 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 { 292 let rec: *u8 = (b as i64 + voff) as *u8 293 fo[0] = 0 as u8; fr[0] = 0 as u8; fus[0] = 0 as u8 294 rb_field(rec, vl, "obj" as *u8, fo, 256) 295 rb_field(rec, vl, "rel" as *u8, fr, 128) 296 rb_field(rec, vl, "sub" as *u8, fus, 256) 297 if rb_seq(fo, obj) == 1 { if rb_seq(fr, rel) == 1 { if rb_has_hash(fus) == 1 { 298 if rb_tuple_current(store, obj, rel, fus) == 1 { 299 let gobj: *u8 = sys_mmap(256); let grel: *u8 = sys_mmap(128) 300 rb_split_userset(fus, gobj, grel) 301 if rb_check2(p1, p2, sub, grel, gobj, depth + 1) == 1 { return 1 } 302 } 303 } } } 304 } 305 } 306 j = voff + vl 307 } 308 s = s + 1 309 } 310 return 0 311} 312func rb_check2(p1: *u8, p2: *u8, sub: *u8, perm: *u8, obj: *u8, depth: i64) -> i64 { 313 if depth > RB_MAXDEPTH { return 0 } 314 let ot: *u8 = sys_mmap(64) 315 rb_otype(obj, ot, 64) 316 if rb_seq(ot, "user" as *u8) == 1 { if rb_tuple_current2(p1, p2, obj, "blocked" as *u8, sub) == 1 { return 0 } } 317 let rels: *i64 = sys_mmap(8 * 16) as *i64 318 let nr: i64 = rb_expand(ot, perm, rels, 16) 319 var ri: i64 = 0 320 while ri < nr { 321 let rel: *u8 = rels[ri] as *u8 322 if rb_tuple_current2(p1, p2, obj, rel, sub) == 1 { return 1 } 323 if rb_userset_scan(p1, p1, p2, sub, obj, rel, depth) == 1 { return 1 } 324 if rb_userset_scan(p2, p1, p2, sub, obj, rel, depth) == 1 { return 1 } 325 ri = ri + 1 326 } 327 return 0 328} 329 330// WRITE AUTHORITY: may `grantor` write tuples on `obj`? = they can MANAGE it (owner/admin), OR the object has NO 331// owner yet (bootstrap: first grantor claims it). Prevents a member self-granting admin (the escalation adversary). 332func rb_obj_has_owner(prefix: *u8, obj: *u8) -> i64 { 333 let segp: *i64 = sys_mmap(16) as *i64 334 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: authz must see the WHOLE store (a truncated walk = wrong last-write-wins) 335 let segs: *i64 = segp[0] as *i64 336 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fs: *u8 = sys_mmap(256); let fc: *u8 = sys_mmap(8) 337 var found: i64 = 0 338 var s: i64 = 0 339 while s < ns { 340 let path: *u8 = sys_mmap(512); var po: i64 = 0 341 po = rb_cat(path, po, prefix); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8 342 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0] 343 var j: i64 = 0 344 while j + 9 <= sz { 345 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4 346 if b[j] == 1 { 347 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 { 348 let rec: *u8 = (b as i64 + voff) as *u8 349 fo[0] = 0 as u8; fr[0] = 0 as u8 350 rb_field(rec, vl, "obj" as *u8, fo, 256) 351 rb_field(rec, vl, "rel" as *u8, fr, 128) 352 if rb_seq(fo, obj) == 1 { if rb_seq(fr, "owner" as *u8) == 1 { found = 1 } } 353 } 354 } 355 j = voff + vl 356 } 357 s = s + 1 358 } 359 return found 360} 361func rb_may_grant(prefix: *u8, grantor: *u8, obj: *u8) -> i64 { 362 if rb_obj_has_owner(prefix, obj) == 0 { return 1 } // unclaimed -> bootstrap first grant 363 if rb_check(prefix, grantor, "manage" as *u8, obj, 0) == 1 { return 1 } 364 return 0 365} 366 367// ---- session uid -> registered handle (the tie to Nishi HR identity; generic, any surface uses it) ---- 368// hex-encode n bytes of src into dst (lowercase, NUL-term); returns chars written (2n). 369func rb_hex(dst: *u8, src: *u8, n: i64) -> i64 { 370 let hx: *u8 = "0123456789abcdef" as *u8 371 var o: i64 = 0 372 var i: i64 = 0 373 while i < n { 374 let c: i64 = (src[i] as i64) & 0xff 375 dst[o] = hx[(c >> 4) & 15]; dst[o + 1] = hx[c & 15] 376 o = o + 2; i = i + 1 377 } 378 dst[o] = 0 as u8 379 return o 380} 381// uidhex -> handle from the login daemon's index (TAB-separated: field0=uidhex, field1=handle; '#'=comment). 382func rb_idx_lookup(idx: *u8, idxlen: i64, uidhex: *u8, uxn: i64, out_h: *u8, cap: i64) -> i64 { 383 var ls: i64 = 0 384 while ls < idxlen { 385 var le: i64 = ls 386 while le < idxlen { if (idx[le] as i64) == 10 { break } le = le + 1 } 387 if le > ls { if (idx[ls] as i64) != 35 { 388 var tab: i64 = ls 389 while tab < le { if (idx[tab] as i64) == 9 { break } tab = tab + 1 } 390 if tab < le { 391 let f0len: i64 = tab - ls 392 if f0len == uxn { 393 var m: i64 = 1 394 var k: i64 = 0 395 while k < uxn { if idx[ls + k] != uidhex[k] { m = 0; k = uxn } else { k = k + 1 } } 396 if m == 1 { 397 var o: i64 = 0 398 var q: i64 = tab + 1 399 while q < le { if o < cap - 1 { out_h[o] = idx[q]; o = o + 1 } q = q + 1 } 400 out_h[o] = 0 as u8 401 return o 402 } 403 } 404 } 405 } } 406 ls = le + 1 407 } 408 out_h[0] = 0 as u8 409 return 0 410} 411// resolve a raw uid (uidn bytes, from olg_whoami) to its registered handle via the on-disk index. 0 = unmapped. 412// SINGLE EXIT + PAIRED FREES (2026-08-19): this runs ONCE PER AUTHENTICATED REQUEST in every surface that imports 413// it (office, project, ...) and used to leave the whole index map plus two scratch pages resident per call -- the 414// per-request mmap-leak class (nx_opaque_login, 177 GB, 2026-08-18). ss_freeall re-derives ss_readall's mapping. 415const RB_SZP_SZ: i64 = 16 416const RB_UXBUF_SZ: i64 = 128 417func rb_resolve_handle(idxpath: *u8, uid: *u8, uidn: i64, out_h: *u8, cap: i64) -> i64 { 418 let szp: *i64 = sys_mmap(RB_SZP_SZ) as *i64 419 let idx: *u8 = ss_readall(idxpath, szp) 420 var n: i64 = 0 421 if (idx as i64) == 0 { out_h[0] = 0 as u8 } else { 422 let uxbuf: *u8 = sys_mmap(RB_UXBUF_SZ) 423 let uxn: i64 = rb_hex(uxbuf, uid, uidn) 424 n = rb_idx_lookup(idx, szp[0], uxbuf, uxn, out_h, cap) 425 sys_munmap(uxbuf, RB_UXBUF_SZ) 426 ss_freeall(idx, szp[0]) 427 } 428 sys_munmap(szp as *u8, RB_SZP_SZ) 429 return n 430} 431 432// is string s already among the first n pointers of out[]? (dedup guard for the enumerators below) 433func rb_in_list(out: *i64, n: i64, s: *u8) -> i64 { 434 var i: i64 = 0 435 while i < n { if rb_seq(out[i] as *u8, s) == 1 { return 1 } i = i + 1 } 436 return 0 437} 438// enumerate the DISTINCT CURRENT subjects of (obj, rel) into out[] (fresh mmap'd copies). returns count (<=cap). 439// e.g. rb_list_subjects(p, "user:alice", "friend", ...) = alice's current friends. Presentation-agnostic. 440func rb_list_subjects(prefix: *u8, obj: *u8, rel: *u8, out: *i64, cap: i64) -> i64 { 441 var n: i64 = 0 442 let segp: *i64 = sys_mmap(16) as *i64 443 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: authz must see the WHOLE store 444 let segs: *i64 = segp[0] as *i64 445 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fs: *u8 = sys_mmap(256) 446 var s: i64 = 0 447 while s < ns { 448 let path: *u8 = sys_mmap(512); var po: i64 = 0 449 po = rb_cat(path, po, prefix); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8 450 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0] 451 var j: i64 = 0 452 while j + 9 <= sz { 453 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4 454 if b[j] == 1 { 455 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 { 456 let rec: *u8 = (b as i64 + voff) as *u8 457 fo[0] = 0 as u8; fr[0] = 0 as u8; fs[0] = 0 as u8 458 rb_field(rec, vl, "obj" as *u8, fo, 256) 459 rb_field(rec, vl, "rel" as *u8, fr, 128) 460 rb_field(rec, vl, "sub" as *u8, fs, 256) 461 if rb_seq(fo, obj) == 1 { if rb_seq(fr, rel) == 1 { 462 if n < cap { if rb_in_list(out, n, fs) == 0 { if rb_tuple_current(prefix, obj, rel, fs) == 1 { 463 let cp: *u8 = sys_mmap(256); var q: i64 = 0 464 while fs[q] != (0 as u8) { cp[q] = fs[q]; q = q + 1 } cp[q] = 0 as u8 465 out[n] = cp as i64; n = n + 1 466 } } } 467 } } 468 } 469 } 470 j = voff + vl 471 } 472 s = s + 1 473 } 474 return n 475} 476// enumerate the DISTINCT CURRENT objects where (obj, rel, sub) holds, into out[]. returns count (<=cap). 477// e.g. rb_list_objects_for_sub(p, "user:alice", "member", ...) = the groups alice is a member of. 478// HISTORY, in append(time) order: every subject that has EVER held `rel` on `obj`, tombstoned ones included, 479// duplicates kept because a thing can come back to a previous holder. rb_list_subjects answers "who holds 480// this NOW"; this answers "whose hands has this passed through", which is a different question and the one 481// provenance needs. ADDED 2026-07-25 by the share-plane retirement: the append-only log already contained 482// this answer and nothing could read it, so a gift ledger would have had to keep its own second copy of 483// history -- which is how the duplicate plane got built in the first place. 484func rb_list_subjects_history(prefix: *u8, obj: *u8, rel: *u8, out: *i64, cap: i64) -> i64 { 485 var n: i64 = 0 486 let segp: *i64 = sys_mmap(16) as *i64 487 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: a truncated walk is a rewritten history 488 let segs: *i64 = segp[0] as *i64 489 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fs: *u8 = sys_mmap(256); let fc: *u8 = sys_mmap(16) 490 var s: i64 = 0 491 while s < ns { 492 let path: *u8 = sys_mmap(512); var po: i64 = 0 493 po = rb_cat(path, po, prefix); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8 494 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0] 495 var j: i64 = 0 496 while j + 9 <= sz { 497 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4 498 if b[j] == 1 { 499 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 { 500 let rec: *u8 = (b as i64 + voff) as *u8 501 fo[0] = 0 as u8; fr[0] = 0 as u8; fs[0] = 0 as u8; fc[0] = 0 as u8 502 rb_field(rec, vl, "obj" as *u8, fo, 256) 503 rb_field(rec, vl, "rel" as *u8, fr, 128) 504 rb_field(rec, vl, "sub" as *u8, fs, 256) 505 rb_field(rec, vl, "cur" as *u8, fc, 16) 506 if rb_seq(fo, obj) == 1 { if rb_seq(fr, rel) == 1 { 507 // only the ESTABLISHING writes form the chain; a tombstone records the end of a 508 // holding, not a new holder, so counting it would double every hop. 509 if fc[0] == (49 as u8) { 510 if n < cap { 511 let cp: *u8 = sys_mmap(256); var q: i64 = 0 512 while fs[q] != (0 as u8) { cp[q] = fs[q]; q = q + 1 } cp[q] = 0 as u8 513 out[n] = cp as i64; n = n + 1 514 } 515 } 516 } } 517 } 518 } 519 j = voff + vl 520 } 521 s = s + 1 522 } 523 return n 524} 525func rb_list_objects_for_sub(prefix: *u8, sub: *u8, rel: *u8, out: *i64, cap: i64) -> i64 { 526 var n: i64 = 0 527 let segp: *i64 = sys_mmap(16) as *i64 528 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: authz must see the WHOLE store 529 let segs: *i64 = segp[0] as *i64 530 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fs: *u8 = sys_mmap(256) 531 var s: i64 = 0 532 while s < ns { 533 let path: *u8 = sys_mmap(512); var po: i64 = 0 534 po = rb_cat(path, po, prefix); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8 535 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0] 536 var j: i64 = 0 537 while j + 9 <= sz { 538 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4 539 if b[j] == 1 { 540 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 { 541 let rec: *u8 = (b as i64 + voff) as *u8 542 fo[0] = 0 as u8; fr[0] = 0 as u8; fs[0] = 0 as u8 543 rb_field(rec, vl, "obj" as *u8, fo, 256) 544 rb_field(rec, vl, "rel" as *u8, fr, 128) 545 rb_field(rec, vl, "sub" as *u8, fs, 256) 546 if rb_seq(fs, sub) == 1 { if rb_seq(fr, rel) == 1 { 547 if n < cap { if rb_in_list(out, n, fo) == 0 { if rb_tuple_current(prefix, fo, rel, sub) == 1 { 548 let cp: *u8 = sys_mmap(256); var q: i64 = 0 549 while fo[q] != (0 as u8) { cp[q] = fo[q]; q = q + 1 } cp[q] = 0 as u8 550 out[n] = cp as i64; n = n + 1 551 } } } 552 } } 553 } 554 } 555 j = voff + vl 556 } 557 s = s + 1 558 } 559 return n 560}