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}