code wiki / (root) / nx_crash.nx

nx_crash.nx source

↩ module page · 193 lines · 11065 B

1// nx_crash.nx -- Captures and prints instruction and fault addresses on signal crashes, then re-raises the signal. 2const K_MAGIC_65536: i64 = 65536 3const K_MAGIC_32768: i64 = 32768 4// nx_crash.nx -- RUNTIME CRASH DIAGNOSTICS (lang climb v11 rung 1a + v12 rung 1b, 2026-08-13). 5// 6// WHY THIS EXISTS. The 2026-08-13 full-population transcript mine (1,090 sessions / 2.88 GiB) 7// ranked silent SEGV debugging as the single biggest language-attributable token burn: 5,151 8// hits across 376 sessions, and every one of them was a program that printed NOTHING when it 9// died. One call at main entry -- nx_crash_guard() -- turns SIGSEGV / SIGBUS / SIGILL / SIGFPE 10// into a 5W+H diagnostic naming the INSTRUCTION ADDRESS and the FAULT ADDRESS, then lets the 11// ORIGINAL signal end the process, so wait-status semantics are UNCHANGED: a watcher that 12// expects death-by-signal-11 still sees death-by-signal-11. 13// 14// SELF-CONTAINED BY DESIGN (v12 rung 1b): zero imports, so the default-on driver injection can 15// append this ONE file to any translation unit without dragging the full nx_syscalls wrapper 16// set into minimal binaries. The two tiny shims below use RAW x86-64 numbers (write=1, mmap=9) 17// -- the SAME constants nx_syscalls.nx selects under @ifdef TARGET_X86_64 -- because this lane 18// is x86-only: the RV64 backend does not inject and must not import this file. 19// 20// DECLARED LIMITS (named, not hidden): 21// * addresses print raw -- resolve to file:line offline with the .debug_line decoder on a -g 22// build (the encoder/decoder pair shipped 2026-08-07, nx_dwline_roundtrip_gate 13/13); 23// * handler is async-signal-safe by construction: write(2) only, stack buffers, no 24// allocation in the handler, no locks. (Stack overflow IS covered: see sigaltstack below.) 25// 26// MECHANISM. rt_sigaction (syscall 13) with the kernel's 4-word struct sigaction: 27// [0]=sa_sigaction [1]=sa_flags [2]=sa_restorer [3]=sa_mask (sigsetsize=8) 28// sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTORER | SA_RESETHAND = 0x8C000004. SA_RESETHAND 29// restores the DEFAULT disposition before the handler runs, so the re-raise below takes the 30// kernel's real kill path. sa_restorer must be non-null for frame setup but is NEVER EXECUTED 31// -- this handler does not return (it unblocks the signal and re-raises it instead), which is 32// exactly what makes a NishiLang function legal here: an ordinary prologue would break 33// rt_sigreturn's frame contract, so the design forbids returning rather than fighting the ABI. 34// Kernel delivery ABI (x86-64): handler(sig in rdi, siginfo* in rsi, ucontext* in rdx) -- 35// matches the NishiLang 3-param convention; PROVEN by the witness printing sig=11 and the 36// EXACT planted fault address (nx_probe_crash_live.nx derefs address 64 -> "0x...40"). 37// Frame offsets (x86-64 signal frame, verified by that witness, not assumed): 38// siginfo: si_addr at byte 16 -> si[2] 39// ucontext: uc_mcontext.rip at byte 168 -> uc[21] (40-byte ucontext head + gregs[16]) 40// ucontext: uc_mcontext.rsp at byte 160 -> uc[20] (drives the stack-overflow classifier) 41 42// stderr write + anonymous mmap, raw x86-64 (see header). 43func nxcr_write(s: *u8, n: i64) -> i64 { 44 return __syscall(1, 2, s as i64, n, 0, 0, 0) 45} 46func nxcr_mmap(size: i64) -> i64 { 47 return __syscall(9, 0, size, 3, 0x22, 0 - 1, 0) 48} 49 50// stderr puts, async-signal-safe (runtime strlen; no hand-counted lengths -- the sys_write 51// count class is banked as a trap). 52func nxcr_puts(s: *u8) -> i64 { 53 var n: i64 = 0 54 while s[n] != (0 as u8) { n = n + 1 } 55 nxcr_write(s, n) 56 return 0 57} 58 59// 0x + 16 hex nibbles to stderr from a stack buffer. 60func nxcr_puthex(v: i64) -> i64 { 61 var b: [20]u8 62 b[0] = (48 as u8) 63 b[1] = (120 as u8) 64 var i: i64 = 0 65 while i < 16 { 66 var nib: i64 = (v >> ((15 - i) * 4)) & 15 67 var c: i64 = 48 + nib 68 if nib > 9 { c = 87 + nib } 69 b[2 + i] = (c as u8) 70 i = i + 1 71 } 72 nxcr_write(b as *u8, 18) 73 return 0 74} 75 76func nxcr_putdec(v: i64) -> i64 { 77 var b: [24]u8 78 var t: [24]u8 79 var m: i64 = v 80 var k: i64 = 0 81 if m == 0 { t[0] = (48 as u8); k = 1 } 82 while m > 0 { t[k] = ((48 + (m % 10)) as u8); m = m / 10; k = k + 1 } 83 var i: i64 = 0 84 while i < k { b[i] = t[k - 1 - i]; i = i + 1 } 85 nxcr_write(b as *u8, k) 86 return 0 87} 88 89// The handler. NEVER RETURNS (see header). Prints the 5W+H diagnostic, unblocks the signal 90// (delivery auto-blocked it), re-raises -- SA_RESETHAND already restored the default 91// disposition, so the process now dies by the ORIGINAL signal. exit_group is the fail-safe 92// if the re-raise is somehow refused. 93func nx_crash_on_signal(sig: i64, si: *i64, uc: *i64) -> i64 { 94 let ip: i64 = uc[21] 95 let fa: i64 = si[2] 96 nxcr_puts("\nthe program crashed: signal " as *u8) 97 nxcr_putdec(sig) 98 if sig == 11 { nxcr_puts(" (SIGSEGV) -- it touched memory it does not own.\n" as *u8) } 99 if sig == 7 { nxcr_puts(" (SIGBUS) -- it touched memory the hardware refuses (bad alignment or a truncated mapping).\n" as *u8) } 100 if sig == 4 { nxcr_puts(" (SIGILL) -- the processor hit an instruction it cannot execute (usually a wild jump).\n" as *u8) } 101 if sig == 8 { nxcr_puts(" (SIGFPE) -- an arithmetic trap, usually division by zero.\n" as *u8) } 102 nxcr_puts(" where: the instruction at " as *u8) 103 nxcr_puthex(ip) 104 nxcr_puts(", touching address " as *u8) 105 nxcr_puthex(fa) 106 nxcr_puts("\n why you see this: the crash guard printed it and then let the original signal end the program, so the exit status is unchanged.\n" as *u8) 107 // Classify the fault for the fix line: a touched address within 64 KiB of the crashed 108 // thread's stack pointer (rsp = gregs[15] -> uc[20]) is the stack guard page -- almost 109 // always unbounded recursion -- and the null-pointer hint would send the reader to the 110 // wrong class entirely. A wrong hint is worse than no hint; the two cases are separable 111 // mechanically, so separate them. 112 // WARN SELF-CAUGHT 2026-08-13: these two branches fired for EVERY signal, so a SIGFPE printed 113 // "a touched address at or near zero is a null pointer" -- the null hint on an ARITHMETIC 114 // trap, which is the misdirection this guard's own header forbids ("a wrong hint is worse 115 // than no hint"). The memory branches now fire only for the memory signals; FPE and ILL 116 // carry their own. Found by probing constant divide-by-zero, not by review. 117 var mem_sig: i64 = 0 118 if sig == 11 { mem_sig = 1 } 119 if sig == 7 { mem_sig = 1 } 120 if sig == 8 { 121 nxcr_puts(" fix: an arithmetic trap on x86 is almost always DIVISION (or remainder) BY ZERO -- check the divisor on the line the address above resolves to. It is also produced by dividing the most-negative integer by -1, whose true result does not fit.\n" as *u8) 122 } 123 if sig == 4 { 124 nxcr_puts(" fix: the processor reached bytes that are not a valid instruction -- usually a call through a function pointer that was never assigned, or a jump through a corrupted value. Check the last function pointer this code stored or loaded.\n" as *u8) 125 } 126 let sp_now: i64 = uc[20] 127 var sp_delta: i64 = fa - sp_now 128 if sp_delta < 0 { sp_delta = 0 - sp_delta } 129 if mem_sig == 1 { 130 if sp_delta < K_MAGIC_65536 { 131 nxcr_puts(" fix: the touched address sits at the stack boundary -- this is almost always UNBOUNDED RECURSION (stack overflow). Find the call that recurses without a base case; the instruction address above names the frame that overflowed.\n" as *u8) 132 } 133 if sp_delta >= K_MAGIC_65536 { 134 nxcr_puts(" fix: a touched address at or near zero is a null or sentinel-zero pointer -- check the last pointer this code computed. Rebuild with -g and resolve the instruction address to file:line with the .debug_line decoder.\n" as *u8) 135 nxcr_puts(" note: raw pointers (*T) are the unchecked C-class lane. CHECKED access already exists -- fixed arrays [N]T and slices []T refuse an out-of-bounds touch with a message instead of crashing. capability=memory-safety-enforcement, roadmap: nishifamily.com/compare/lang\n" as *u8) 136 } 137 } 138 // Name the program and the EXACT resolver command, so the reader's next step is copy-paste: 139 // nx_addr2line <elf> <ip> prints file:line AND the function name on a -g build, and REFUSES 140 // (rc=4) on a build without .debug_line -- then the fix is one rebuild with -g. In-process 141 // resolution is deliberately NOT embedded: it would drag the DWARF decoder into every ~5 KB 142 // guarded binary; the one-command offline loop is the design. readlink(/proc/self/exe) = x86 89. 143 var pbuf: [256]u8 144 let pth: *u8 = "/proc/self/exe" as *u8 145 let pn: i64 = __syscall(89, pth as i64, (pbuf as *u8) as i64, 255, 0, 0, 0) 146 if pn > 0 { 147 nxcr_puts(" resolve: nx_addr2line " as *u8) 148 nxcr_write(pbuf as *u8, pn) 149 nxcr_puts(" " as *u8) 150 nxcr_puthex(ip) 151 nxcr_puts(" (file:line + function on a -g build; refuses without .debug_line, then rebuild with -g)\n" as *u8) 152 } 153 var mword: i64 = 1 << (sig - 1) 154 __syscall(14, 1, (&mword) as i64, 0, 8, 0, 0) 155 let pid: i64 = __syscall(172, 0, 0, 0, 0, 0, 0) // rv64 getpid=172; raw x86 39 is an RV64 KEY translated to ioctl -> -ENOTTY (debt idx 2277) 156 __syscall(129, pid, sig, 0, 0, 0, 0) // rv64 kill=129; raw x86 62 is an RV64 KEY translated to lseek, so this re-raise NEVER FIRED and every guarded crash left via exit_group(128+sig) instead of dying by its signal (debt idx 2277) 157 __syscall(231, 128 + sig, 0, 0, 0, 0, 0) 158 return 0 159} 160 161func nxcr_install_one(sig: i64, h: i64) -> i64 { 162 let act: *i64 = nxcr_mmap(64) as *i64 163 act[0] = h 164 // SA_SIGINFO | SA_ONSTACK | SA_RESTORER | SA_RESETHAND -- ONSTACK routes delivery onto the 165 // alternate stack installed below, which is the ONLY way a STACK-OVERFLOW SIGSEGV can run a 166 // handler at all (the faulting thread has no room left on its own stack). 167 act[1] = 0x8C000004 168 act[2] = h 169 act[3] = 0 170 return __syscall(13, sig, act as i64, 0, 8, 0, 0) 171} 172 173// One call at main entry arms the guard for the four fatal fault signals. 174// Installs a 32 KiB ALTERNATE SIGNAL STACK first (sigaltstack, syscall 131; stack_t = 175// {ss_sp, ss_flags, ss_size}) so the handler runs even when the crash IS stack exhaustion -- 176// without this, a stack-overflow SEGV cannot push the handler frame and the process dies 177// silently. Idempotent: re-running just reinstalls the same dispositions, so an explicit 178// caller plus the default-on injection is harmless. 179func nx_crash_guard() -> i64 { 180 let sp: i64 = nxcr_mmap(K_MAGIC_32768) 181 let ss: *i64 = nxcr_mmap(32) as *i64 182 ss[0] = sp 183 ss[1] = 0 184 ss[2] = K_MAGIC_32768 185 __syscall(131, ss as i64, 0, 0, 0, 0, 0) 186 let h: func(i64, *i64, *i64) -> i64 = nx_crash_on_signal 187 let ha: i64 = h as i64 188 nxcr_install_one(11, ha) 189 nxcr_install_one(7, ha) 190 nxcr_install_one(4, ha) 191 nxcr_install_one(8, ha) 192 return 0 193}