code wiki / (root) / nx_probe.nx

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}