code wiki / (root) / nx_abi_lock.nx

nx_abi_lock.nx source

↩ module page · 186 lines · 7258 B

1// nx_abi_lock.nx -- sealed signature of a module's public ABI surface. 2// 3// LAYER 2 stability primitive paired with nx_module_cas. The IDE at 4// nishifamily.com/ide pins each user program to BOTH the module CAS 5// root (which proves bytes-equal) AND the upstream-substrate ABI lock 6// (which proves "the public surface I depend on still exists and has 7// the same shape"). An update that bumps internal bytes but doesn't 8// alter the ABI returns NX_ABI_UNCHANGED -- safe to auto-rebind. A 9// pure addition returns NX_ABI_ADDITIVE_OK. Anything else BREAKING_* 10// and the IDE refuses to silently upgrade the user's pinned version. 11// 12// ABI signature shape (canonical bytes): 13// for each public symbol, sorted by name lex-ascending: 14// uvarint(name_len) || name_utf8 || 15// uvarint(n_params) || 16// uvarint(return_type_id) || 17// for each param i in 0..n_params-1: 18// uvarint(param_type_id_i) 19// abi_signature = sha256(concat above) 20// 21// Type IDs are NishiLang TypeKind values (TY_VOID=0, TY_I64=4, etc. 22// from ir.h). Substrate type-table is stable; type IDs do not change 23// release to release. When adding a new TypeKind we add it at the end 24// of the enum so old IDs stay valid -- cardinal global rule 19. 25// 26// Sealed verdict for ABI compat check (NX_ABI_*): 27// 0 INCONCLUSIVE missing/empty symbol table on one side 28// 1 BREAKING_REMOVED old symbol absent from new manifest 29// 2 BREAKING_RETYPED same name but signature bytes differ 30// 3 BREAKING_RESERIALIZED same name + arity but ret/param type id changed 31// 4 ADDITIVE_OK every old symbol present, identical, plus new ones 32// 5 UNCHANGED identical signature hashes both sides 33// 34// Cardinal global rule 19 (API Contract Stability) is enforced at the 35// IDE bind point: the loader compares the user's pinned NX_ABI hash 36// to the live substrate's hash. UNCHANGED or ADDITIVE_OK proceed 37// silently; anything else paginates the user with a named diff and 38// refuses to load until they re-pin. 39// 40// genealogy_id: semver_2_0_0 + abi_compat_lessons_glibc + rust_semver_trick + 41// cabal_solver_pvp + apk_diff_compat_layer 42// lineage_id: public_abi_lock_q10 43 44import "nx_syscalls.nx" 45import "nx_tier.nx" 46import "nx_sha256.nx" 47 48const NX_ABI_HASH_BYTES: nx_int = 32 49 50// Sealed compat verdict. 51const NX_ABI_INCONCLUSIVE: nx_int = 0 52const NX_ABI_BREAKING_REMOVED: nx_int = 1 53const NX_ABI_BREAKING_RETYPED: nx_int = 2 54const NX_ABI_BREAKING_RESERIALIZED: nx_int = 3 55const NX_ABI_ADDITIVE_OK: nx_int = 4 56const NX_ABI_UNCHANGED: nx_int = 5 57const NX_ABI_N_VERDICTS: nx_int = 6 58 59// Public symbol descriptor. Caller populates name_bytes + sig_bytes 60// before hashing; sig_bytes is the canonical encoding above for that 61// one symbol. 62struct AbiSymbol { 63 name_bytes: *u8, 64 name_len: nx_int, 65 sig_bytes: *u8, // canonical bytes for THIS symbol 66 sig_len: nx_int, 67} 68 69// Whole-module ABI manifest. symbols must be sorted by name ascending 70// before hashing; we leave that to the caller because Nishi has no 71// generic in-place sort yet. signature_hash is the 32-byte sha256 of 72// the concatenated symbol sig_bytes. 73struct AbiManifest { 74 module_name: *u8, 75 module_name_len: nx_int, 76 symbols: *AbiSymbol, 77 n_symbols: nx_int, 78 signature_hash: *u8, // 32 bytes; filled by nx_abi_lock_seal 79} 80 81// Compute signature_hash by hashing each symbol's sig_bytes in order. 82func nx_abi_lock_seal(am: *AbiManifest) -> nx_int { 83 var total: nx_int = 0 84 var i: nx_int = 0 85 while i < am.n_symbols { 86 total = total + am.symbols[i].sig_len 87 i = i + 1 88 } 89 let buf: *u8 = (sys_mmap(total)) as *u8 90 var off: nx_int = 0 91 i = 0 92 while i < am.n_symbols { 93 var k: nx_int = 0 94 let n: nx_int = am.symbols[i].sig_len 95 let src: *u8 = am.symbols[i].sig_bytes 96 while k < n { 97 buf[off + k] = src[k] 98 k = k + 1 99 } 100 off = off + n 101 i = i + 1 102 } 103 sha256_digest(buf, total, am.signature_hash) 104 return 0 105} 106 107// Compare two ABI manifests and emit a sealed verdict. Both manifests 108// must have already been sealed (signature_hash populated). 109// 110// Algorithm: if both signature_hash buffers are byte-equal, 111// UNCHANGED. Otherwise walk old.symbols and try to find each in 112// new.symbols by name (linear scan -- substrate has no map yet); 113// missing = BREAKING_REMOVED, sig_bytes mismatch = BREAKING_RETYPED. 114// After all old symbols accounted for, surplus new symbols are 115// purely additive => ADDITIVE_OK. 116func nx_abi_lock_compare(old: *AbiManifest, new_m: *AbiManifest) -> nx_int { 117 if old.n_symbols == 0 { return NX_ABI_INCONCLUSIVE } 118 if new_m.n_symbols == 0 { return NX_ABI_INCONCLUSIVE } 119 120 // Fast path: signature hashes match -> UNCHANGED. 121 var same: nx_int = 1 122 var k: nx_int = 0 123 while k < 32 { 124 if old.signature_hash[k] != new_m.signature_hash[k] { same = 0 } 125 k = k + 1 126 } 127 if same == 1 { return NX_ABI_UNCHANGED } 128 129 // Walk old symbols, locate each in new. 130 var i: nx_int = 0 131 while i < old.n_symbols { 132 let os: *AbiSymbol = old.symbols[i] 133 var found: nx_int = 0 134 var sig_match: nx_int = 0 135 var j: nx_int = 0 136 while j < new_m.n_symbols { 137 let ns: *AbiSymbol = new_m.symbols[j] 138 // Name equal? Length first, then byte-for-byte. 139 if ns.name_len == os.name_len { 140 var b: nx_int = 0 141 var name_eq: nx_int = 1 142 while b < os.name_len { 143 if os.name_bytes[b] != ns.name_bytes[b] { name_eq = 0 } 144 b = b + 1 145 } 146 if name_eq == 1 { 147 found = 1 148 // Signature equal? 149 if ns.sig_len == os.sig_len { 150 var s: nx_int = 0 151 sig_match = 1 152 while s < os.sig_len { 153 if os.sig_bytes[s] != ns.sig_bytes[s] { sig_match = 0 } 154 s = s + 1 155 } 156 } 157 } 158 } 159 j = j + 1 160 } 161 if found == 0 { return NX_ABI_BREAKING_REMOVED } 162 if sig_match == 0 { return NX_ABI_BREAKING_RETYPED } 163 i = i + 1 164 } 165 166 // Every old symbol present with identical sig; surplus in new is 167 // additive. 168 return NX_ABI_ADDITIVE_OK 169} 170 171// Sealed-enum validity predicate. Used by nx_self_audit to catch out- 172// of-band values that would mean a caller forged a verdict. 173func nx_abi_verdict_is_valid(v: nx_int) -> nx_int { 174 if v < 0 { return 0 } 175 if v >= NX_ABI_N_VERDICTS { return 0 } 176 return 1 177} 178 179// One-liner predicate that the IDE loader calls before silently 180// rebinding a user's pinned program to a newer substrate. Returns 1 181// if the upgrade is safe (UNCHANGED or ADDITIVE_OK), 0 otherwise. 182func nx_abi_upgrade_is_safe(v: nx_int) -> nx_int { 183 if v == NX_ABI_UNCHANGED { return 1 } 184 if v == NX_ABI_ADDITIVE_OK { return 1 } 185 return 0 186}