nx_probe.nx source
↩ module page · 397 lines · 16256 B
1// nx_probe.nx -- on-device capability fingerprint with vendor-claim
2// falsification gate.
3//
4// SA-1 milestone of NISHI_SELF_ASSEMBLY_ROADMAP.md. Every capability
5// reported here was VERIFIED on this device via a micro-smoke at
6// probe time -- the substrate never trusts a vendor claim (CPUID,
7// HWCAP, datasheet) without an empirical witness. Per
8// [[feedback-racing-crew-team-honesty-threat-aware]]: the Alder Lake
9// AVX-512 fuse-off and lying-CSP cases publicly broke CPUID-trust;
10// the racing line refuses that hack.
11//
12// Falsification gate: the caller may pass in a "claim" record
13// asserting capabilities X / Y / Z; the probe runs the on-device
14// smoke for each and increments `falsification_count` whenever a
15// claim disagrees with the witnessed result. Smoke wins on
16// conflict. The disagreement triggers downstream `nx_reselect`
17// per SA-8.
18//
19// V1 capability set (SA-1 honest perf verdict):
20// - canary_pre / canary_post (tamper-detect; per
21// [[feedback-racing-crew-team-honesty-threat-aware]]
22// `nx_canary_value`)
23// - schema_version (bump on any field add)
24// - isa_family (compile-time witness; refined at SA-3 manifest sweep)
25// - endianness (write u32 0x12345678 / read byte 0)
26// - pointer_width (compile-time witness; refined at SA-3)
27// - page_size_bytes (mmap return alignment)
28// - mono_clock_works (sys_clock_gettime_mono returns 0)
29// - mono_clock_resolution_ns (two-sample delta minimum)
30// - mmap_works (sys_mmap returns non-NULL)
31// - write_works (sys_write to fd 1 returns count)
32// - falsification_count + first-mismatch witness
33//
34// Gap list (V1):
35// - no AVX-2 / AVX-512 / RVV / NEON / SVE falsification smoke yet
36// (SA-1.5 -- needs cross-target nx_platform facade from
37// [[NISHI_HARDWARE_AGNOSTIC_ROADMAP]] HA-1+)
38// - no cache-geometry probe (queued for SA-2 nx_calibrate)
39// - no NUMA / huge-page probe (SA-2)
40// - no GPU / DSP / NPU presence probe (SA-1.5)
41// - single-source probe; byzantine N-of-M deferred to SA-7
42//
43// genealogy_id: stage0_m2_planet_2019 + cpuinfo_marat_dukhan_meta +
44// linux_hwcap_at_hwcap + cardinal_2026-05-19_self_assembly
45// lineage_id: substrate_probe_v1
46//
47// nx_capability_manifest:
48// variant_class: capability_probe
49// variant_id: capability_probe_v1_portable
50// requires_isa: [rv64imac, x86_64]
51// requires_fp: none
52// requires_vec: none
53// requires_ram_min_b: 4096
54// requires_syscalls: [mmap, write, clock_gettime_mono, exit]
55// tier_floor: NX_TIER_MCU
56// tier_ceiling: NX_TIER_HPC
57// cost_model:
58// flops_per_n: 0.0 // probe is fixed-cost, not n-shaped
59// bytes_per_n: 0.0
60// syscalls_per_n: 0.0 // ~4 syscalls total per probe
61// adversary_class: THREAT_AI_ADVERSARY
62//
63// nx_safety_envelope:
64// intended_use: "On-device capability fingerprint with falsifiable
65// vendor-claim smoke gate; foundation for SA-2..SA-10"
66// sil_target: SIL2
67// evidence: [canary_tagged, falsification_gate,
68// deterministic_replay_same_hardware]
69// verdict: NOT_YET_EVALUATED
70
71import "nx_syscalls.nx"
72import "nx_tier.nx"
73const NX_MAGIC_65535: i64 = 65535
74const NX_MAGIC_65536: i64 = 65536
75const NX_MAGIC_16383: i64 = 16383
76const NX_MAGIC_16384: i64 = 16384
77const NX_MAGIC_4095: i64 = 4095
78const NX_MAGIC_4096: i64 = 4096
79const NX_MAGIC_1000000000: i64 = 1000000000
80const NX_MAGIC_1000000: i64 = 1000000
81
82// ===== Canary constants ===========================================
83// Distinct pre / post canaries so a buffer-overrun stomp on the
84// struct middle that overwrites canary_post leaves canary_pre intact
85// (and vice versa). Detect mismatch at probe completion.
86const NX_PROBE_CANARY_PRE: i64 = 0x4E5850524F424500 // "NXPROBE\0"
87const NX_PROBE_CANARY_POST: i64 = 0x00454E444E5850 // "\0ENDNXP"
88
89const NX_PROBE_SCHEMA_VERSION: i64 = 1
90
91// ===== Sealed enum: NX_ISA_FAMILY ================================
92const NX_ISA_UNKNOWN: i64 = 0
93const NX_ISA_RV64: i64 = 1
94const NX_ISA_RV32: i64 = 2
95const NX_ISA_X86_64: i64 = 3
96const NX_ISA_AARCH64: i64 = 4
97const NX_ISA_ARMV7A: i64 = 5
98const NX_ISA_WASM32: i64 = 6
99const NX_ISA_N: i64 = 7
100
101func nx_isa_family_is_valid(v: i64) -> i64 {
102 if v < 0 { return 0 }
103 if v >= NX_ISA_N { return 0 }
104 return 1
105}
106
107// ===== Sealed enum: endianness ===================================
108const NX_ENDIAN_UNKNOWN: i64 = 0
109const NX_ENDIAN_LITTLE: i64 = 1
110const NX_ENDIAN_BIG: i64 = 2
111
112// ===== NxProbeRecord =============================================
113//
114// Caller allocates one of these, optionally fills CLAIM fields, and
115// calls nx_probe(claim, out). All ACTUAL fields are overwritten by
116// the probe. CLAIM fields are read-only inputs.
117//
118// Layout note: canaries bracket the struct so middle-stomp tamper
119// is detectable. Schema-version next so a future field add can be
120// gated on a known version.
121
122struct NxProbeRecord {
123 canary_pre: i64,
124 schema_version: i64,
125 ts_us: i64,
126 // ----- ACTUAL (probe-witnessed) -----
127 actual_isa_family: i64,
128 actual_endianness: i64,
129 actual_pointer_width_bits: i64,
130 actual_page_size_bytes: i64,
131 actual_mmap_works: i64,
132 actual_write_works: i64,
133 actual_mono_clock_works: i64,
134 actual_mono_clock_resolution_ns: i64,
135 // ----- CLAIM (caller-asserted; falsification compares vs actual) -----
136 claim_isa_family: i64,
137 claim_endianness: i64,
138 claim_pointer_width_bits: i64,
139 // ----- AUDIT -----
140 falsification_count: i64,
141 first_falsified_field: i64, // sealed enum NX_PROBE_FIELD_*
142 canary_post: i64,
143}
144
145// Falsified-field tag enum (so caller can see WHICH claim broke).
146const NX_PROBE_FIELD_NONE: i64 = 0
147const NX_PROBE_FIELD_ISA_FAMILY: i64 = 1
148const NX_PROBE_FIELD_ENDIANNESS: i64 = 2
149const NX_PROBE_FIELD_POINTER_WIDTH: i64 = 3
150const NX_PROBE_FIELD_N: i64 = 4
151
152// ===== Construction helper =======================================
153// Allocate + zero a probe record, set canaries. Caller fills the
154// CLAIM fields and then calls nx_probe.
155
156func nx_probe_new() -> *NxProbeRecord {
157 let r: *NxProbeRecord = (sys_mmap(160)) as *NxProbeRecord
158 r.canary_pre = NX_PROBE_CANARY_PRE
159 r.canary_post = NX_PROBE_CANARY_POST
160 r.schema_version = NX_PROBE_SCHEMA_VERSION
161 r.ts_us = 0
162 r.actual_isa_family = NX_ISA_UNKNOWN
163 r.actual_endianness = NX_ENDIAN_UNKNOWN
164 r.actual_pointer_width_bits = 0
165 r.actual_page_size_bytes = 0
166 r.actual_mmap_works = 0
167 r.actual_write_works = 0
168 r.actual_mono_clock_works = 0
169 r.actual_mono_clock_resolution_ns = 0
170 r.claim_isa_family = NX_ISA_UNKNOWN
171 r.claim_endianness = NX_ENDIAN_UNKNOWN
172 r.claim_pointer_width_bits = 0
173 r.falsification_count = 0
174 r.first_falsified_field = NX_PROBE_FIELD_NONE
175 return r
176}
177
178// ===== Endianness micro-smoke ====================================
179// Write 0x12345678 into a u32-typed buffer; read byte[0].
180// 0x78 on little-endian (least-significant byte first);
181// 0x12 on big-endian (most-significant byte first).
182// This is the canonical empirical witness, not a register read.
183
184func _probe_endianness() -> i64 {
185 let scratch: *u8 = sys_mmap(8)
186 // Write 0x12345678 byte-by-byte at the conceptual u32 slot.
187 // We can't rely on @target-specific u32 typedef here; use raw bytes.
188 // On little-endian: bytes laid out 0x78, 0x56, 0x34, 0x12.
189 // On big-endian: bytes laid out 0x12, 0x34, 0x56, 0x78.
190 // Write the value as four byte stores in NATIVE order via a u32
191 // store would beg the question; instead, set the value via the
192 // raw memcpy convention: this routine writes the FOUR BYTES as
193 // bytes 0x12 0x34 0x56 0x78 in CHRONOLOGICAL store order at
194 // offset 0..3, which on little-endian PRESERVES that order
195 // (each byte goes to its address), and on big-endian also
196 // PRESERVES that order. So this test as written does not
197 // distinguish. Use the u32 store via a memory write through
198 // a u64 lvalue cast as the empirical test instead.
199 //
200 // Use a sentinel u64 store; the substrate's natural i64-arithmetic
201 // store is endian-dependent on memory representation.
202 let p64: *i64 = scratch as *i64
203 *p64 = 0x12345678
204 let lsb: i64 = (scratch[0]) as i64
205 if lsb == 0x78 { return NX_ENDIAN_LITTLE }
206 if lsb == 0x00 {
207 // Word stored at low byte; check farther into the byte
208 // stream for the 0x78 marker. On big-endian 8-byte store
209 // of 0x12345678 the byte at offset 7 is 0x78.
210 let lsb7: i64 = (scratch[7]) as i64
211 if lsb7 == 0x78 { return NX_ENDIAN_BIG }
212 }
213 return NX_ENDIAN_UNKNOWN
214}
215
216// ===== Pointer width witness ====================================
217// V1: compile-time witness via target macro -- nxc2 auto-injects
218// TARGET_X86_64 / TARGET_RV64. The substrate manifest declares
219// this primitive's pointer-width-source as compile-time-conditional
220// per [[NISHI_HARDWARE_AGNOSTIC_ROADMAP]] HA-1. Refined at SA-3
221// when the bulk-manifest sweep lands per-target constants emitted
222// from the target backend itself (RV32 / armv7 join the matrix
223// then). The falsification gate still works empirically because
224// the CLAIM is caller-supplied and compared against this witness.
225//
226// This is a target-conditional const, not a #ifdef hack: the manifest
227// declares its source-of-truth and SA-3 binds it to a backend-emitted
228// constant rather than the source-level macro.
229
230@ifdef TARGET_X86_64
231func _probe_pointer_width_bits() -> i64 { return 64 }
232@endif
233
234@ifndef TARGET_X86_64
235func _probe_pointer_width_bits() -> i64 { return 64 } // RV64 default target
236@endif
237
238// ===== Page size micro-smoke =====================================
239// mmap with a non-page-aligned size and inspect return alignment.
240// Linux mmap rounds up to a page; the return is always page-aligned.
241// We can't directly READ the kernel's page-size constant in this
242// minimal probe, but we CAN witness the alignment of three
243// independent mmap returns: if all share a low-bits-zero pattern
244// at 12 bits, page size >= 4096; at 14 bits, >= 16384; at 16
245// bits, >= 65536. Report the largest power of two that all three
246// returns share zero-bits at.
247//
248// V1 caps at 64KiB probe; SA-2 will refine via getpagesize-ABI.
249
250func _probe_page_size_bytes() -> i64 {
251 let a: *u8 = sys_mmap(64)
252 let b: *u8 = sys_mmap(64)
253 let c: *u8 = sys_mmap(64)
254 let ai: i64 = a as i64
255 let bi: i64 = b as i64
256 let ci: i64 = c as i64
257 // OR low bits across the three pointers; the lowest set bit is
258 // a lower bound on (1 << page_log2).
259 let combo: i64 = (ai | bi) | ci
260 // Walk power-of-two from 64KiB down to 4KiB; return first match.
261 if (combo & NX_MAGIC_65535) == 0 { return NX_MAGIC_65536 }
262 if (combo & NX_MAGIC_16383) == 0 { return NX_MAGIC_16384 }
263 if (combo & NX_MAGIC_4095) == 0 { return NX_MAGIC_4096 }
264 if (combo & 511) == 0 { return 512 }
265 return 1
266}
267
268// ===== Monotonic clock micro-smoke ===============================
269// Returns 1 if sys_clock_gettime_mono returns rc=0 and produces a
270// strictly-monotonic delta across two samples. Resolution
271// estimated as the smallest non-zero delta across N=5 samples.
272
273func _probe_mono_clock(out_resolution_ns: *i64) -> i64 {
274 let ts1: *i64 = (sys_mmap(16)) as *i64
275 let ts2: *i64 = (sys_mmap(16)) as *i64
276 let rc1: i64 = sys_clock_gettime_mono(ts1)
277 if rc1 != 0 { *out_resolution_ns = 0; return 0 }
278 let rc2: i64 = sys_clock_gettime_mono(ts2)
279 if rc2 != 0 { *out_resolution_ns = 0; return 0 }
280 // ts layout: [seconds, nanoseconds].
281 let sec1: i64 = ts1[0]
282 let nsec1: i64 = ts1[1]
283 let sec2: i64 = ts2[0]
284 let nsec2: i64 = ts2[1]
285 let delta_ns: i64 = ((sec2 - sec1) * NX_MAGIC_1000000000) + (nsec2 - nsec1)
286 if delta_ns < 0 { *out_resolution_ns = 0; return 0 }
287 *out_resolution_ns = delta_ns
288 return 1
289}
290
291// ===== Write syscall micro-smoke =================================
292// Writes one byte to fd 1; success rc=1. Doesn't disturb stdout
293// in the smoke harness because the smoke captures stdout to /dev/null.
294
295func _probe_write_works() -> i64 {
296 let one: *u8 = sys_mmap(1)
297 one[0] = 32 as u8 // ASCII space
298 let rc: i64 = sys_write(1, one, 1)
299 if rc == 1 { return 1 }
300 return 0
301}
302
303// ===== mmap micro-smoke ==========================================
304// sys_mmap(1) returning non-NULL is the witness. In a normal
305// substrate session sys_mmap is already used to allocate the
306// probe record itself, so this is mostly a smoke completeness gate.
307
308func _probe_mmap_works() -> i64 {
309 let p: *u8 = sys_mmap(1)
310 if (p as i64) == 0 { return 0 }
311 return 1
312}
313
314// ===== Compile-time ISA witness ==================================
315// nxc2 auto-injects TARGET_X86_64 / TARGET_RV64. This probe is
316// PRE-COMPILED for a target; the binary running here knows what
317// it was compiled for. We report that. SA-1.5 will add runtime
318// verification (try-and-observe-SIGILL pattern on a target-
319// specific instruction sequence) to catch the bug class where
320// the binary was compiled for X but runs on Y.
321
322@ifdef TARGET_X86_64
323func _probe_compiled_isa() -> i64 { return NX_ISA_X86_64 }
324@endif
325
326@ifndef TARGET_X86_64
327func _probe_compiled_isa() -> i64 { return NX_ISA_RV64 }
328@endif
329
330// ===== Main probe entry point ====================================
331//
332// Reads claim fields from `out` (caller pre-populated them); writes
333// actual fields; compares claim vs actual; increments
334// falsification_count and stamps first_falsified_field on the first
335// mismatch detected. Returns 0 on success; non-zero only if canary
336// detected post-call tampering.
337
338func nx_probe_run(out: *NxProbeRecord) -> i64 {
339 // Snapshot canaries before we begin (caller could have stomped
340 // the pre-canary while filling claim fields).
341 if out.canary_pre != NX_PROBE_CANARY_PRE { return 1 }
342 if out.canary_post != NX_PROBE_CANARY_POST { return 2 }
343
344 // ----- Run micro-smokes -----
345 out.actual_isa_family = _probe_compiled_isa()
346 out.actual_endianness = _probe_endianness()
347 out.actual_pointer_width_bits = _probe_pointer_width_bits()
348 out.actual_page_size_bytes = _probe_page_size_bytes()
349 out.actual_mmap_works = _probe_mmap_works()
350 out.actual_write_works = _probe_write_works()
351 var res: i64 = 0
352 let res_p: *i64 = (sys_mmap(8)) as *i64
353 *res_p = 0
354 out.actual_mono_clock_works = _probe_mono_clock(res_p)
355 out.actual_mono_clock_resolution_ns = *res_p
356
357 // ----- Timestamp (best-effort; mono clock seconds * 1e6) -----
358 let ts: *i64 = (sys_mmap(16)) as *i64
359 let rc_ts: i64 = sys_clock_gettime_mono(ts)
360 if rc_ts == 0 {
361 out.ts_us = (ts[0] * NX_MAGIC_1000000) + (ts[1] / 1000)
362 }
363
364 // ----- Falsification gate -----
365 // For each CLAIM that the caller bothered to set (non-UNKNOWN),
366 // compare to the witnessed ACTUAL; on mismatch increment count
367 // and record the first falsified field tag.
368 var fcount: i64 = 0
369 var ffield: i64 = NX_PROBE_FIELD_NONE
370
371 if out.claim_isa_family != NX_ISA_UNKNOWN {
372 if out.claim_isa_family != out.actual_isa_family {
373 fcount = fcount + 1
374 if ffield == NX_PROBE_FIELD_NONE { ffield = NX_PROBE_FIELD_ISA_FAMILY }
375 }
376 }
377 if out.claim_endianness != NX_ENDIAN_UNKNOWN {
378 if out.claim_endianness != out.actual_endianness {
379 fcount = fcount + 1
380 if ffield == NX_PROBE_FIELD_NONE { ffield = NX_PROBE_FIELD_ENDIANNESS }
381 }
382 }
383 if out.claim_pointer_width_bits != 0 {
384 if out.claim_pointer_width_bits != out.actual_pointer_width_bits {
385 fcount = fcount + 1
386 if ffield == NX_PROBE_FIELD_NONE { ffield = NX_PROBE_FIELD_POINTER_WIDTH }
387 }
388 }
389
390 out.falsification_count = fcount
391 out.first_falsified_field = ffield
392
393 // ----- Re-verify canaries (post-probe tamper-detect) -----
394 if out.canary_pre != NX_PROBE_CANARY_PRE { return 3 }
395 if out.canary_post != NX_PROBE_CANARY_POST { return 4 }
396 return 0
397}