code wiki / (root) / nx_dbg_step_gate.nx

nx_dbg_step_gate.nx source

↩ module page · 648 lines · 34255 B

1// nx_dbg_step_gate.nx -- the VERDICT gate for LN12 (nx_dbg_step, watch symbol dbg_step_over). 2// 3// THE ACCEPT RULE (/compare/lang LN12): break and step on our own binaries over the line table we 4// emit. So this gate does not mock anything: 5// * it ASSEMBLES a real RV64 program containing a real linking CALL, 6// * it EMITS that program's .debug_line with nx_dwarf_line.nx -- OUR OWN DWARF v5 emitter, the one 7// LB5 shipped and GNU readelf validates -- so the table under test is the table we produce, 8// * it runs the deployed nx_dbg_step.elf against the pair and compares against expectations that 9// the debugger had no part in computing. 10// 11// THE SUBJECT IS NEVER ITS OWN ORACLE. Every expected value comes from somewhere else: 12// breakpoint address = the gate's OWN fixture layout (it placed line 12 at offset 8; the debugger 13// has to rediscover that through the line table, and the two must agree). 14// callee / caller line = likewise the gate's layout. 15// exit code and halt = a SECOND, DEBUGGER-FREE sim (bootcap_machine) run in this process. 16// never-executed addr = not assumed -- the reference sim records every pc it fetches and the gate 17// PROVES the neg-control address is absent from that set before using it. 18// the call is a call = decoded arithmetically with rv64im_min_decoder (kind == JAL, rd == ra) 19// BEFORE any stepping outcome is asserted, so a fixture that could not 20// exercise step-over cannot score a pass. 21// 22// WHY THE DISCRIMINATING TOOTH IS THE ONE THAT MATTERS. A debugger that single-steps INSTRUCTIONS 23// passes "the line changed" trivially. The only thing that separates a stepping debugger from an 24// instruction stepper is that STEP-OVER MUST NOT REPORT THE CALLEE'S LINE while STEP-INTO MUST. Both 25// are asserted here, on the same fixture, from the same starting pc -- two tests that each isolate one 26// signal would not prove discrimination between them. 27// 28// NEVER-BRICK IS TESTED BEHAVIOURALLY, NOT BY GREPPING FOR A WORD. A pid-shaped argument must be 29// REFUSED (a process id can never become a subject), and the source must contain no sys_ptrace call 30// site. The behavioural half cannot be fooled by prose, which a source scan can be -- this file and 31// the organ both discuss ptrace at length, and a detector that finds its own explanation of the bug 32// it hunts is the trap this estate has already paid for twice. 33// 34// Fixtures live in /tmp/nx_dbg_step_gate/ (created at setup, so a crashed run cannot leave a fixture 35// beside a production beat) and are ASSEMBLED AT RUNTIME, never checked in. 36// license_tier: ORIGINAL 37import "nx_syscalls.nx" 38import "nx_gate_verdict.nx" 39import "nx_gatekit_lib.nx" 40import "nx_dwarf_line.nx" 41import "nx_bootcap.nx" 42import "rv64im_min_decoder.nx" 43 44const DG_DIR: *u8 = "/tmp/nx_dbg_step_gate" 45const DG_IMG: *u8 = "/tmp/nx_dbg_step_gate/fixture.bin" 46const DG_LT: *u8 = "/tmp/nx_dbg_step_gate/fixture.debug_line" 47const DG_SRCNAME: *u8 = "dbgfix.nx" 48const DG_SRC: *u8 = "buildroot/runtime/nx_dbg_step.nx" 49 50// A session over a 10-instruction fixture finishes in milliseconds; a minute is a hang, and a hung 51// subject must become a FAILED TOOTH, not a hung gate. 52const DG_RUN_MS: i64 = 60000 53const DG_POLL_MS: i64 = 25 54const DG_OUT_CAP: i64 = 1048576 // the gatekit capture bound; asserted not reached, never silent 55const DG_ARGV_MAX: i64 = 16 56const DG_MODE_644: i64 = 420 57const DG_ASCII_ZERO: i64 = 48 58const DG_ASCII_NINE: i64 = 57 59const DG_ASCII_a: i64 = 97 60const DG_ASCII_f: i64 = 102 61const DG_DECIMAL: i64 = 10 62const DG_HEXBASE: i64 = 16 63const DG_PTR: i64 = 8 64const DG_SCRATCH: i64 = 64 65const DG_BYTE_MASK: i64 = 255 66 67// ---- the fixture, as DATA. offsets are 4-byte instruction slots; lines are the source lines the 68// emitted table will carry. Every expectation below is derived from THIS table, never from a literal 69// repeated at the assertion site. 70const DG_N_INSTR: i64 = 10 71const DG_OFF_L10: i64 = 0 // addi a0, x0, 7 72const DG_OFF_L11: i64 = 4 // addi a1, x0, 2 73const DG_OFF_CALL: i64 = 8 // jal ra, +24 <- THE CALL 74const DG_OFF_AFTER: i64 = 12 // addi a2, x0, 3 <- where a step-OVER must land 75const DG_OFF_SETRC: i64 = 16 // addi a0, x0, 5 76const DG_OFF_HALT: i64 = 20 // ebreak <- halt_code = a0 77const DG_OFF_NEVER: i64 = 24 // addi a3, x0, 9 <- NEVER executed (neg-control) 78const DG_OFF_NEVER2: i64 = 28 // ebreak <- NEVER executed 79const DG_OFF_CALLEE: i64 = 32 // addi a4, x0, 4 <- where a step-INTO must land 80const DG_OFF_CALLRET: i64 = 36 // jalr x0, 0(ra) 81const DG_IMG_BYTES: i64 = 40 82 83const DG_LINE_L10: i64 = 10 84const DG_LINE_L11: i64 = 11 85const DG_LINE_CALL: i64 = 12 86const DG_LINE_AFTER: i64 = 13 87const DG_LINE_SETRC: i64 = 14 88const DG_LINE_HALT: i64 = 15 89const DG_LINE_NEVER: i64 = 20 90const DG_LINE_NEVER2: i64 = 21 91const DG_LINE_CALLEE: i64 = 100 92const DG_LINE_CALLRET: i64 = 101 93const DG_LINE_BOGUS: i64 = 999 // present in no row: the resolver must refuse it BY NAME 94const DG_END_OFF: i64 = 40 // end_sequence address = one past the last instruction 95 96// ---- RV64 encodings. Hand-built HERE on purpose: the gate must derive its fixture without help 97// from the subject, and an encoder shared with the debugger would make the fixture agree with it by 98// construction rather than by correctness. 99const DG_OPC_OP_IMM: i64 = 0x13 100const DG_OPC_JAL: i64 = 0x6F 101const DG_OPC_JALR: i64 = 0x67 102const DG_EBREAK: i64 = 0x00100073 103const DG_REG_RA: i64 = 1 104const DG_REG_A0: i64 = 10 105const DG_REG_A1: i64 = 11 106const DG_REG_A2: i64 = 12 107const DG_REG_A3: i64 = 13 108const DG_REG_A4: i64 = 14 109const DG_IMM12_MASK: i64 = 4095 110const DG_EXIT_RC: i64 = 5 // what the fixture puts in a0 before ebreak 111const DG_A0_INITIAL: i64 = 7 // deliberately different, so a wrong exit code cannot look right 112 113func dg_addi(rd: i64, rs1: i64, imm: i64) -> i64 { 114 return ((imm & DG_IMM12_MASK) << 20) | (rs1 << 15) | (rd << 7) | DG_OPC_OP_IMM 115} 116func dg_jal(rd: i64, off: i64) -> i64 { 117 let b20: i64 = (off >> 20) & 1 118 let b10_1: i64 = (off >> 1) & 1023 119 let b11: i64 = (off >> 11) & 1 120 let b19_12: i64 = (off >> 12) & DG_BYTE_MASK 121 return (b20 << 31) | (b10_1 << 21) | (b11 << 20) | (b19_12 << 12) | (rd << 7) | DG_OPC_JAL 122} 123func dg_jalr(rd: i64, rs1: i64, imm: i64) -> i64 { 124 return ((imm & DG_IMM12_MASK) << 20) | (rs1 << 15) | (rd << 7) | DG_OPC_JALR 125} 126func dg_put32(b: *u8, off: i64, w: i64) -> i64 { 127 var i: i64 = 0 128 while i < 4 { b[off + i] = ((w >> (i * 8)) & DG_BYTE_MASK) as u8; i = i + 1 } 129 return 0 130} 131func dg_get32(b: *u8, off: i64) -> i64 { 132 var v: i64 = 0 133 var i: i64 = 0 134 while i < 4 { v = v | ((b[off + i] as i64) << (i * 8)); i = i + 1 } 135 return v 136} 137 138// ---- output plumbing 139func dg_p(s: *u8) -> i64 { return gv_puts(s) } 140func dg_n(v: i64) -> i64 { return gv_num(v) } 141 142func dg_write_bin(path: *u8, buf: *u8, n: i64) -> i64 { 143 let fd: i64 = sys_openat_wr(path, DG_MODE_644) 144 if fd < 0 { return 0 - 1 } 145 gk_write_all(fd, buf, n) 146 sys_close(fd) 147 return n 148} 149 150// spawn the subject with an arbitrary-arity argv (gk_run_capture tops out at four, and a debugger 151// script is longer than that), stdout+stderr to a log. Returns the pid. 152func dg_spawn(elf: *u8, words: *i64, nwords: i64, logpath: *u8) -> i64 { 153 let argv: *i64 = sys_mmap(DG_PTR * (DG_ARGV_MAX + 1)) as *i64 154 argv[0] = elf as i64 155 var i: i64 = 0 156 while i < nwords { argv[1 + i] = words[i]; i = i + 1 } 157 argv[1 + nwords] = 0 158 let envp: *i64 = sys_mmap(DG_PTR * 2) as *i64 159 envp[0] = 0 160 let pid: i64 = sys_fork() 161 if pid < 0 { return 0 - 1 } 162 if pid == 0 { 163 let lf: i64 = sys_openat_wr(logpath, DG_MODE_644) 164 if lf >= 0 { sys_dup3(lf, 1, 0); sys_dup3(lf, 2, 0) } 165 sys_execve(elf, argv, envp) 166 sys_exit(127) 167 } 168 return pid 169} 170// bounded reap: the exit code, or -1 if it had to be killed (announced, and a failed tooth) 171func dg_reap(pid: i64, budget_ms: i64) -> i64 { 172 let st: *i64 = sys_mmap(DG_SCRATCH) as *i64 173 var waited: i64 = 0 174 while waited < budget_ms { 175 st[0] = 0 176 let r: i64 = sys_wait4(pid, st, WNOHANG) 177 if r == pid { return gk_wait_code(st[0]) } 178 sys_sleep_ms(DG_POLL_MS) 179 waited = waited + DG_POLL_MS 180 } 181 dg_p(" WATCHDOG: subject did not exit within budget -- killed (a failed tooth, never a pass)\n" as *u8) 182 nx_kill(pid, 9) 183 sys_wait4(pid, st, 0) 184 return 0 - 1 185} 186 187// run one debugger session; returns the exit code and fills outbuf/outlen 188func dg_session(elf: *u8, words: *i64, nwords: i64, log: *u8, outbuf: *u8, outlen: *i64) -> i64 { 189 let pid: i64 = dg_spawn(elf, words, nwords, log) 190 if pid < 0 { outlen[0] = 0; return 0 - 1 } 191 let rc: i64 = dg_reap(pid, DG_RUN_MS) 192 let n: i64 = gk_read(log, outbuf, DG_OUT_CAP - 1) 193 var m: i64 = n 194 if m < 0 { m = 0 } 195 outbuf[m] = 0 as u8 196 outlen[0] = m 197 return rc 198} 199 200// ---- output readers. Anchored: find the ANCHOR first, then the field after it, so a field name 201// occurring earlier in the transcript can never be read as this line's answer. 202func dg_find(buf: *u8, n: i64, from: i64, needle: *u8) -> i64 { 203 let nl: i64 = gk_len(needle) 204 if nl == 0 { return 0 - 1 } 205 var i: i64 = from 206 while i + nl <= n { 207 var j: i64 = 0 208 var ok: i64 = 1 209 while j < nl { if buf[i + j] != needle[j] { ok = 0; j = nl } else { j = j + 1 } } 210 if ok == 1 { return i } 211 i = i + 1 212 } 213 return 0 - 1 214} 215// decimal value of `field` that appears AFTER `anchor`; found[0]=0 when either is missing 216func dg_dec_after(buf: *u8, n: i64, anchor: *u8, field: *u8, found: *i64) -> i64 { 217 found[0] = 0 218 var at: i64 = 0 219 if gk_len(anchor) > 0 { 220 at = dg_find(buf, n, 0, anchor) 221 if at < 0 { return 0 } 222 at = at + gk_len(anchor) 223 } 224 let f: i64 = dg_find(buf, n, at, field) 225 if f < 0 { return 0 } 226 var i: i64 = f + gk_len(field) 227 var v: i64 = 0 228 var d: i64 = 0 229 var go: i64 = 1 230 while go == 1 { 231 if i >= n { go = 0; continue } 232 let c: i64 = buf[i] as i64 233 if c < DG_ASCII_ZERO { go = 0; continue } 234 if c > DG_ASCII_NINE { go = 0; continue } 235 v = v * DG_DECIMAL + (c - DG_ASCII_ZERO) 236 d = d + 1 237 i = i + 1 238 } 239 if d == 0 { return 0 } 240 found[0] = 1 241 return v 242} 243func dg_hex_after(buf: *u8, n: i64, anchor: *u8, field: *u8, found: *i64) -> i64 { 244 found[0] = 0 245 var at: i64 = 0 246 if gk_len(anchor) > 0 { 247 at = dg_find(buf, n, 0, anchor) 248 if at < 0 { return 0 } 249 at = at + gk_len(anchor) 250 } 251 let f: i64 = dg_find(buf, n, at, field) 252 if f < 0 { return 0 } 253 var i: i64 = f + gk_len(field) 254 var v: i64 = 0 255 var d: i64 = 0 256 var go: i64 = 1 257 while go == 1 { 258 if i >= n { go = 0; continue } 259 let c: i64 = buf[i] as i64 260 var h: i64 = 0 - 1 261 if c >= DG_ASCII_ZERO { if c <= DG_ASCII_NINE { h = c - DG_ASCII_ZERO } } 262 if c >= DG_ASCII_a { if c <= DG_ASCII_f { h = c - (DG_ASCII_a - DG_DECIMAL) } } 263 if h < 0 { go = 0; continue } 264 v = v * DG_HEXBASE + h 265 d = d + 1 266 i = i + 1 267 } 268 if d == 0 { return 0 } 269 found[0] = 1 270 return v 271} 272func dg_has(buf: *u8, n: i64, needle: *u8) -> i64 { 273 if dg_find(buf, n, 0, needle) >= 0 { return 1 } 274 return 0 275} 276 277// ---- decimal word for argv (breakpoint line numbers are passed as text) 278func dg_word(v: i64) -> *u8 { 279 let b: *u8 = sys_mmap(DG_SCRATCH) 280 let t: *u8 = sys_mmap(DG_SCRATCH) 281 var m: i64 = v 282 var k: i64 = 0 283 if m == 0 { t[0] = DG_ASCII_ZERO as u8; k = 1 } 284 while m > 0 { t[k] = (DG_ASCII_ZERO + (m % DG_DECIMAL)) as u8; m = m / DG_DECIMAL; k = k + 1 } 285 var i: i64 = 0 286 while i < k { b[i] = t[k - 1 - i]; i = i + 1 } 287 b[k] = 0 as u8 288 return b 289} 290 291func main() -> i64 { 292 gv_head("nx_dbg_step gate -- statement stepping on a real RV64 fixture over a line table emitted by our own nx_dwarf_line; expectations from the gate's fixture layout and a debugger-free reference sim; neg-controls for a never-executed breakpoint, a bogus file:line, and a pid-shaped subject" as *u8) 293 let ctr: *i64 = gv_ctr() 294 gk_mkdir(DG_DIR) 295 296 // ---- resolve the subject 297 let elf: *u8 = sys_mmap(1024) 298 var have: i64 = 0 299 if gk_exists("_offc/nx_dbg_step.elf" as *u8) == 1 { gk_cat(elf, 0, "_offc/nx_dbg_step.elf" as *u8); elf[gk_len("_offc/nx_dbg_step.elf" as *u8)] = 0 as u8; have = 1 } 300 if have == 0 { if gk_exists("./nx_dbg_step.elf" as *u8) == 1 { gk_cat(elf, 0, "./nx_dbg_step.elf" as *u8); elf[gk_len("./nx_dbg_step.elf" as *u8)] = 0 as u8; have = 1 } } 301 if have == 0 { if gk_exists("_build/nx_dbg_step.sov.elf" as *u8) == 1 { gk_cat(elf, 0, "_build/nx_dbg_step.sov.elf" as *u8); elf[gk_len("_build/nx_dbg_step.sov.elf" as *u8)] = 0 as u8; have = 1 } } 302 dg_p(" subject: " as *u8) 303 if have == 1 { dg_p(elf) } else { dg_p("ABSENT (looked in _offc/, ./, _build/)" as *u8) } 304 dg_p("\n" as *u8) 305 gv_need("deployed nx_dbg_step.elf (the subject)" as *u8, have, ctr) 306 if have == 0 { return gv_verdict("DBG-STEP-GATE" as *u8, ctr, "subject absent" as *u8) } 307 308 // ===================================================================================== 309 // ASSEMBLE THE FIXTURE 310 // ===================================================================================== 311 let img: *u8 = sys_mmap(DG_IMG_BYTES) 312 dg_put32(img, DG_OFF_L10, dg_addi(DG_REG_A0, 0, DG_A0_INITIAL)) 313 dg_put32(img, DG_OFF_L11, dg_addi(DG_REG_A1, 0, 2)) 314 dg_put32(img, DG_OFF_CALL, dg_jal(DG_REG_RA, DG_OFF_CALLEE - DG_OFF_CALL)) 315 dg_put32(img, DG_OFF_AFTER, dg_addi(DG_REG_A2, 0, 3)) 316 dg_put32(img, DG_OFF_SETRC, dg_addi(DG_REG_A0, 0, DG_EXIT_RC)) 317 dg_put32(img, DG_OFF_HALT, DG_EBREAK) 318 dg_put32(img, DG_OFF_NEVER, dg_addi(DG_REG_A3, 0, 9)) 319 dg_put32(img, DG_OFF_NEVER2, DG_EBREAK) 320 dg_put32(img, DG_OFF_CALLEE, dg_addi(DG_REG_A4, 0, 4)) 321 dg_put32(img, DG_OFF_CALLRET, dg_jalr(0, DG_REG_RA, 0)) 322 dg_write_bin(DG_IMG, img, DG_IMG_BYTES) 323 324 let base: i64 = BOOTCAP_MEM_BASE 325 let addr_call: i64 = base + DG_OFF_CALL 326 let addr_after: i64 = base + DG_OFF_AFTER 327 let addr_callee: i64 = base + DG_OFF_CALLEE 328 let addr_never: i64 = base + DG_OFF_NEVER 329 330 // ---- FIXTURE PRECONDITIONS, ASSERTED ARITHMETICALLY AND FIRST. A fixture whose call site is 331 // not a linking call cannot exercise step-over at all, and every outcome tooth below would then 332 // pass for the wrong reason. 333 let w_call: i64 = dg_get32(img, DG_OFF_CALL) 334 let k_call: i64 = nx_rv64im_decode_kind(w_call) 335 let rd_call: i64 = nx_rv64im_rd(w_call) 336 dg_p(" fixture call word=" as *u8); dg_n(w_call) 337 dg_p(" decoded_kind=" as *u8); dg_n(k_call); dg_p(" (JAL=" as *u8); dg_n(NX_RV64IM_OP_JAL); dg_p(")" as *u8) 338 dg_p(" rd=" as *u8); dg_n(rd_call); dg_p(" (ra=" as *u8); dg_n(DG_REG_RA); dg_p(")\n" as *u8) 339 var call_ok: i64 = 0 340 if k_call == NX_RV64IM_OP_JAL { if rd_call == DG_REG_RA { call_ok = 1 } } 341 gv_check("fixture-call-site-decodes-as-a-LINKING-jal-rd-is-ra-so-step-over-has-something-to-step-over" as *u8, call_ok, ctr) 342 343 let w_ret: i64 = dg_get32(img, DG_OFF_CALLRET) 344 let k_ret: i64 = nx_rv64im_decode_kind(w_ret) 345 let rd_ret: i64 = nx_rv64im_rd(w_ret) 346 let rs1_ret: i64 = nx_rv64im_rs1(w_ret) 347 dg_p(" fixture ret word=" as *u8); dg_n(w_ret) 348 dg_p(" kind=" as *u8); dg_n(k_ret); dg_p(" (JALR=" as *u8); dg_n(NX_RV64IM_OP_JALR); dg_p(")" as *u8) 349 dg_p(" rd=" as *u8); dg_n(rd_ret); dg_p(" rs1=" as *u8); dg_n(rs1_ret); dg_p("\n" as *u8) 350 var ret_ok: i64 = 0 351 if k_ret == NX_RV64IM_OP_JALR { if rd_ret == 0 { if rs1_ret == DG_REG_RA { ret_ok = 1 } } } 352 gv_check("fixture-callee-returns-through-jalr-x0-ra-so-the-call-depth-really-comes-back-down" as *u8, ret_ok, ctr) 353 354 // ===================================================================================== 355 // THE REFERENCE RUN -- a SECOND sim, no debugger. Supplies the exit code and PROVES the 356 // neg-control address is genuinely never fetched. 357 // ===================================================================================== 358 let reftx: *u8 = sys_mmap(BOOTCAP_MEM_SIZE) 359 let ref: *NxRv64imSim = bootcap_machine(img, DG_IMG_BYTES, reftx, BOOTCAP_MEM_SIZE) 360 var never_seen: i64 = 0 361 var call_seen: i64 = 0 362 var callee_seen: i64 = 0 363 var refsteps: i64 = 0 364 var rgo: i64 = 1 365 while rgo == 1 { 366 if ref.halted == 1 { rgo = 0; continue } 367 if refsteps >= BOOTCAP_MAX_STEPS { rgo = 0; continue } 368 if ref.pc == addr_never { never_seen = never_seen + 1 } 369 if ref.pc == addr_call { call_seen = call_seen + 1 } 370 if ref.pc == addr_callee { callee_seen = callee_seen + 1 } 371 nx_rv64im_sim_step(ref) 372 refsteps = refsteps + 1 373 } 374 dg_p(" reference run (no debugger): halted=" as *u8); dg_n(ref.halted) 375 dg_p(" exit_code=" as *u8); dg_n(ref.halt_code) 376 dg_p(" steps=" as *u8); dg_n(refsteps) 377 dg_p(" call_pc_fetched=" as *u8); dg_n(call_seen) 378 dg_p(" callee_pc_fetched=" as *u8); dg_n(callee_seen) 379 dg_p(" never_addr_fetched=" as *u8); dg_n(never_seen); dg_p("\n" as *u8) 380 var refclean: i64 = 0 381 if ref.halted == 1 { if ref.halt_code == DG_EXIT_RC { refclean = 1 } } 382 gv_check("reference-sim-halts-clean-and-its-exit-code-is-the-one-the-fixture-stores-not-a-hardcoded-expectation" as *u8, refclean, ctr) 383 var reached: i64 = 0 384 if call_seen > 0 { if callee_seen > 0 { reached = 1 } } 385 gv_check("fixture-reached-the-condition-both-the-call-site-and-the-callee-really-execute" as *u8, reached, ctr) 386 var neverproven: i64 = 0 387 if never_seen == 0 { neverproven = 1 } 388 gv_check("neg-control-precondition-the-never-executed-address-is-PROVEN-unfetched-over-the-whole-reference-run" as *u8, neverproven, ctr) 389 390 // ===================================================================================== 391 // EMIT THE LINE TABLE WITH OUR OWN nx_dwarf_line (LB5's emitter) -- this is the "over the line 392 // table we emit" half of the contract, and it is why this is not a mock. 393 // ===================================================================================== 394 let srcbuf: *u8 = sys_mmap(DG_SCRATCH) 395 let nmlen: i64 = gk_len(DG_SRCNAME) 396 gk_cat(srcbuf, 0, DG_SRCNAME) 397 let foff: *i64 = sys_mmap(DG_PTR * 2) as *i64 398 let flen: *i64 = sys_mmap(DG_PTR * 2) as *i64 399 foff[0] = 0 400 flen[0] = nmlen 401 let d: *NxDwLine = nx_dwline_new(GK_MAGIC_4096) 402 nx_dwline_header(d, srcbuf, foff, flen, 1) 403 nx_dwline_set_address(d, base + DG_OFF_L10) 404 nx_dwline_record(d, base + DG_OFF_L10, 1, DG_LINE_L10) 405 nx_dwline_record(d, base + DG_OFF_L11, 1, DG_LINE_L11) 406 nx_dwline_record(d, base + DG_OFF_CALL, 1, DG_LINE_CALL) 407 nx_dwline_record(d, base + DG_OFF_AFTER, 1, DG_LINE_AFTER) 408 nx_dwline_record(d, base + DG_OFF_SETRC, 1, DG_LINE_SETRC) 409 nx_dwline_record(d, base + DG_OFF_HALT, 1, DG_LINE_HALT) 410 nx_dwline_record(d, base + DG_OFF_NEVER, 1, DG_LINE_NEVER) 411 nx_dwline_record(d, base + DG_OFF_NEVER2, 1, DG_LINE_NEVER2) 412 nx_dwline_record(d, base + DG_OFF_CALLEE, 1, DG_LINE_CALLEE) 413 nx_dwline_record(d, base + DG_OFF_CALLRET, 1, DG_LINE_CALLRET) 414 nx_dwline_advance_pc(d, DG_END_OFF - DG_OFF_CALLRET) 415 nx_dwline_end_sequence(d) 416 nx_dwline_finish(d) 417 let ltbytes: i64 = nx_dwline_size(d) 418 dg_write_bin(DG_LT, nx_dwline_bytes(d), ltbytes) 419 dg_p(" line table emitted by nx_dwarf_line: bytes=" as *u8); dg_n(ltbytes) 420 dg_p(" rows_recorded=" as *u8); dg_n(DG_N_INSTR) 421 dg_p(" file=" as *u8); dg_p(DG_SRCNAME); dg_p("\n" as *u8) 422 423 let out: *u8 = sys_mmap(DG_OUT_CAP) 424 let olen: *i64 = sys_mmap(DG_SCRATCH) as *i64 425 let fnd: *i64 = sys_mmap(DG_SCRATCH) as *i64 426 let words: *i64 = sys_mmap(DG_PTR * (DG_ARGV_MAX + 1)) as *i64 427 428 // ---- SESSION 0: dump the table. The debugger must rediscover every row we recorded, plus the 429 // end_sequence marker, by decoding bytes it did not write. 430 words[0] = "lines" as i64 431 words[1] = DG_LT as i64 432 var rc: i64 = dg_session(elf, words, 2, "/tmp/nx_dbg_step_gate/s0.log" as *u8, out, olen) 433 var n: i64 = olen[0] 434 dg_p(" S0 lines: rc=" as *u8); dg_n(rc); dg_p(" bytes=" as *u8); dg_n(n); dg_p("\n" as *u8) 435 var capfree: i64 = 0 436 if n < DG_OUT_CAP - 1 { capfree = 1 } 437 gv_check("capture-did-not-reach-its-bound-so-no-tooth-below-is-reading-a-truncated-transcript" as *u8, capfree, ctr) 438 let got_rows: i64 = dg_dec_after(out, n, "DBG linetable ok=" as *u8, "rows=" as *u8, fnd) 439 let got_files: i64 = dg_dec_after(out, n, "DBG linetable ok=" as *u8, "files=" as *u8, fnd) 440 let got_dc: i64 = dg_dec_after(out, n, "DBG linetable ok=" as *u8, "decode_complete=" as *u8, fnd) 441 dg_p(" S0 decoded rows=" as *u8); dg_n(got_rows) 442 dg_p(" expected=" as *u8); dg_n(DG_N_INSTR + 1) 443 dg_p(" files=" as *u8); dg_n(got_files) 444 dg_p(" decode_complete=" as *u8); dg_n(got_dc); dg_p("\n" as *u8) 445 var rows_ok: i64 = 0 446 if got_rows == DG_N_INSTR + 1 { rows_ok = 1 } // 10 statement rows + end_sequence 447 gv_check("every-row-our-emitter-wrote-is-recovered-by-the-debuggers-independent-decode-plus-the-end-sequence" as *u8, rows_ok, ctr) 448 var dc_ok: i64 = 0 449 if got_dc == 1 { dc_ok = 1 } 450 gv_check("line-table-decode-is-COMPLETE-so-a-refusal-below-means-absent-and-not-merely-unread" as *u8, dc_ok, ctr) 451 var name_ok: i64 = 0 452 if dg_has(out, n, DG_SRCNAME) == 1 { name_ok = 1 } 453 gv_check("the-v5-file-name-table-is-decoded-so-a-breakpoint-can-name-a-FILE-and-not-only-a-file-index" as *u8, name_ok, ctr) 454 455 // ---- SESSION 1: STEP INTO. break at the call line, continue to it, then step -- which MUST 456 // enter the callee and report the callee's source line. 457 words[0] = "run" as i64 458 words[1] = DG_IMG as i64 459 words[2] = DG_LT as i64 460 words[3] = "break" as i64 461 words[4] = DG_SRCNAME as i64 462 words[5] = dg_word(DG_LINE_CALL) as i64 463 words[6] = "continue" as i64 464 words[7] = "step" as i64 465 words[8] = "detach" as i64 466 rc = dg_session(elf, words, 9, "/tmp/nx_dbg_step_gate/s1.log" as *u8, out, olen) 467 n = olen[0] 468 let bp_addr: i64 = dg_hex_after(out, n, "BREAK ok=1" as *u8, "addr=0x" as *u8, fnd) 469 let bp_found: i64 = fnd[0] 470 let cont_pc: i64 = dg_hex_after(out, n, "STOP cmd=continue" as *u8, "pc=0x" as *u8, fnd) 471 let step_line: i64 = dg_dec_after(out, n, "STOP cmd=step " as *u8, "line=" as *u8, fnd) 472 let step_found: i64 = fnd[0] 473 let s1_exit: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "exit_code=" as *u8, fnd) 474 let s1_halted: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "halted=" as *u8, fnd) 475 let s1_unch: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "code_bytes_unchanged=" as *u8, fnd) 476 let s1_auto: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "auto=" as *u8, fnd) 477 dg_p(" S1 step-into: rc=" as *u8); dg_n(rc) 478 dg_p(" bp_addr=" as *u8); dg_n(bp_addr); dg_p(" expected=" as *u8); dg_n(addr_call) 479 dg_p(" continue_pc=" as *u8); dg_n(cont_pc) 480 dg_p(" step_line=" as *u8); dg_n(step_line); dg_p(" expected=" as *u8); dg_n(DG_LINE_CALLEE) 481 dg_p(" exit_code=" as *u8); dg_n(s1_exit); dg_p(" halted=" as *u8); dg_n(s1_halted) 482 dg_p(" code_unchanged=" as *u8); dg_n(s1_unch); dg_p(" auto_detach=" as *u8); dg_n(s1_auto) 483 dg_p("\n" as *u8) 484 485 var bpok: i64 = 0 486 if bp_found == 1 { if bp_addr == addr_call { bpok = 1 } } 487 gv_check("breakpoint-set-by-FILE-and-LINE-resolves-through-our-line-table-to-the-address-the-fixture-layout-fixes" as *u8, bpok, ctr) 488 var hitok: i64 = 0 489 if cont_pc == addr_call { hitok = 1 } 490 gv_check("that-breakpoint-actually-HITS-and-execution-stops-with-pc-exactly-on-it" as *u8, hitok, ctr) 491 var intook: i64 = 0 492 if step_found == 1 { if step_line == DG_LINE_CALLEE { intook = 1 } } 493 gv_check("step-INTO-from-the-call-statement-enters-the-callee-and-reports-the-callee-source-line" as *u8, intook, ctr) 494 var resumed1: i64 = 0 495 if s1_halted == 1 { if s1_exit == ref.halt_code { resumed1 = 1 } } 496 gv_check("after-detach-the-guest-RESUMES-and-exits-with-the-reference-sims-exit-code-the-safety-tooth" as *u8, resumed1, ctr) 497 var unch1: i64 = 0 498 if s1_unch == 1 { unch1 = 1 } 499 gv_check("guest-code-bytes-are-byte-identical-after-the-session-breakpoints-never-patched-the-guest" as *u8, unch1, ctr) 500 501 // ---- SESSION 2: STEP OVER, from the SAME pc as session 1. This is the discriminating pair: 502 // same fixture, same starting statement, opposite required answer. 503 words[7] = "stepover" as i64 504 rc = dg_session(elf, words, 9, "/tmp/nx_dbg_step_gate/s2.log" as *u8, out, olen) 505 n = olen[0] 506 let over_line: i64 = dg_dec_after(out, n, "STOP cmd=stepover" as *u8, "line=" as *u8, fnd) 507 let over_found: i64 = fnd[0] 508 let over_pc: i64 = dg_hex_after(out, n, "STOP cmd=stepover" as *u8, "pc=0x" as *u8, fnd) 509 let over_depth: i64 = dg_dec_after(out, n, "STOP cmd=stepover" as *u8, "depth=" as *u8, fnd) 510 let s2_exit: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "exit_code=" as *u8, fnd) 511 let s2_halted: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "halted=" as *u8, fnd) 512 dg_p(" S2 step-over: rc=" as *u8); dg_n(rc) 513 dg_p(" line=" as *u8); dg_n(over_line); dg_p(" expected=" as *u8); dg_n(DG_LINE_AFTER) 514 dg_p(" (callee line " as *u8); dg_n(DG_LINE_CALLEE); dg_p(" must NOT appear here)" as *u8) 515 dg_p(" pc=" as *u8); dg_n(over_pc); dg_p(" expected=" as *u8); dg_n(addr_after) 516 dg_p(" depth=" as *u8); dg_n(over_depth) 517 dg_p(" exit_code=" as *u8); dg_n(s2_exit); dg_p(" halted=" as *u8); dg_n(s2_halted) 518 dg_p("\n" as *u8) 519 520 var overok: i64 = 0 521 if over_found == 1 { if over_line == DG_LINE_AFTER { overok = 1 } } 522 gv_check("step-OVER-from-the-call-statement-lands-on-the-next-statement-IN-THE-CALLER" as *u8, overok, ctr) 523 var overpcok: i64 = 0 524 if over_pc == addr_after { overpcok = 1 } 525 gv_check("step-OVER-stops-at-the-return-address-so-the-whole-callee-really-ran-rather-than-being-skipped" as *u8, overpcok, ctr) 526 // THE DISCRIMINATING TOOTH. A debugger that merely single-steps instructions reports the callee 527 // line here; only one that tracks call depth does not. 528 var notcallee: i64 = 1 529 if over_line == DG_LINE_CALLEE { notcallee = 0 } 530 gv_check("neg-control-step-OVER-never-reports-the-CALLEE-line-the-one-behaviour-that-separates-a-statement-stepper-from-an-instruction-stepper" as *u8, notcallee, ctr) 531 // exactly ONE statement, measured as a line delta against the line we started on 532 var onestmt: i64 = 0 533 if over_line - DG_LINE_CALL == 1 { onestmt = 1 } 534 gv_check("step-OVER-advanced-exactly-ONE-source-statement-line-delta-is-one-not-a-jump-to-wherever" as *u8, onestmt, ctr) 535 var depthback: i64 = 0 536 if over_depth == 0 { depthback = 1 } 537 gv_check("call-depth-returned-to-zero-so-the-callees-return-was-observed-and-not-merely-outrun" as *u8, depthback, ctr) 538 var resumed2: i64 = 0 539 if s2_halted == 1 { if s2_exit == ref.halt_code { resumed2 = 1 } } 540 gv_check("after-a-step-over-session-the-guest-also-resumes-and-exits-normally" as *u8, resumed2, ctr) 541 542 // ---- SESSION 3: NEG-CONTROL, a breakpoint on a line the reference run PROVED is never reached. 543 words[5] = dg_word(DG_LINE_NEVER) as i64 544 words[6] = "continue" as i64 545 words[7] = "detach" as i64 546 rc = dg_session(elf, words, 8, "/tmp/nx_dbg_step_gate/s3.log" as *u8, out, olen) 547 n = olen[0] 548 let n3_addr: i64 = dg_hex_after(out, n, "BREAK ok=1" as *u8, "addr=0x" as *u8, fnd) 549 let n3_set: i64 = fnd[0] 550 let n3_halt: i64 = dg_has(out, n, "STOP cmd=continue reason=halt" as *u8) 551 let n3_bp: i64 = dg_has(out, n, "STOP cmd=continue reason=breakpoint" as *u8) 552 let n3_exit: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "exit_code=" as *u8, fnd) 553 dg_p(" S3 neg-control never-executed line: bp_set=" as *u8); dg_n(n3_set) 554 dg_p(" addr=" as *u8); dg_n(n3_addr); dg_p(" expected=" as *u8); dg_n(addr_never) 555 dg_p(" stopped_on_halt=" as *u8); dg_n(n3_halt) 556 dg_p(" stopped_on_breakpoint=" as *u8); dg_n(n3_bp) 557 dg_p(" exit_code=" as *u8); dg_n(n3_exit); dg_p("\n" as *u8) 558 // the breakpoint must be REAL (resolved and armed) or the neg-control proves nothing 559 var n3_real: i64 = 0 560 if n3_set == 1 { if n3_addr == addr_never { n3_real = 1 } } 561 gv_check("neg-control-is-armed-the-never-executed-line-DID-resolve-and-a-breakpoint-was-really-set-there" as *u8, n3_real, ctr) 562 var n3ok: i64 = 0 563 if n3_bp == 0 { if n3_halt == 1 { n3ok = 1 } } 564 gv_check("neg-control-a-breakpoint-on-a-never-executed-line-does-NOT-stop-and-the-guest-runs-to-its-normal-halt" as *u8, n3ok, ctr) 565 566 // ---- SESSION 4: NEG-CONTROL, a bogus file:line. It must be refused BY NAME. A breakpoint that 567 // quietly did not exist reads exactly like code that never ran, which is the failure this whole 568 // rung exists to make impossible. 569 words[5] = dg_word(DG_LINE_BOGUS) as i64 570 rc = dg_session(elf, words, 8, "/tmp/nx_dbg_step_gate/s4.log" as *u8, out, olen) 571 n = olen[0] 572 let n4_named: i64 = dg_has(out, n, "BREAK REFUSED-NO-SUCH-LINE" as *u8) 573 let n4_silent: i64 = dg_has(out, n, "BREAK ok=1" as *u8) 574 dg_p(" S4 neg-control bogus line " as *u8); dg_n(DG_LINE_BOGUS) 575 dg_p(": refused_by_name=" as *u8); dg_n(n4_named) 576 dg_p(" silently_accepted=" as *u8); dg_n(n4_silent) 577 dg_p(" exit_rc=" as *u8); dg_n(rc); dg_p("\n" as *u8) 578 var n4ok: i64 = 0 579 if n4_named == 1 { if n4_silent == 0 { n4ok = 1 } } 580 gv_check("neg-control-a-bogus-file-line-is-REFUSED-BY-NAME-never-silently-ignored" as *u8, n4ok, ctr) 581 var n4rc: i64 = 0 582 if rc != 0 { n4rc = 1 } 583 gv_check("neg-control-that-refusal-also-carries-a-NONZERO-exit-code-so-a-script-cannot-miss-it" as *u8, n4rc, ctr) 584 585 // ---- SESSION 5: DETACH RUNS EVEN WHEN THE SCRIPT NEVER ASKED FOR IT. This is the always-detach 586 // guarantee: a caller that forgets, or a script that ends early, must still leave the subject 587 // resumed and finished -- never stopped. 588 words[0] = "run" as i64 589 words[1] = DG_IMG as i64 590 words[2] = DG_LT as i64 591 words[3] = "step" as i64 592 rc = dg_session(elf, words, 4, "/tmp/nx_dbg_step_gate/s5.log" as *u8, out, olen) 593 n = olen[0] 594 let n5_auto: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "auto=" as *u8, fnd) 595 let n5_found: i64 = fnd[0] 596 let n5_halted: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "halted=" as *u8, fnd) 597 let n5_exit: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "exit_code=" as *u8, fnd) 598 dg_p(" S5 no-detach-in-script: auto=" as *u8); dg_n(n5_auto) 599 dg_p(" halted=" as *u8); dg_n(n5_halted) 600 dg_p(" exit_code=" as *u8); dg_n(n5_exit); dg_p(" expected=" as *u8); dg_n(ref.halt_code); dg_p("\n" as *u8) 601 var n5ok: i64 = 0 602 if n5_found == 1 { if n5_auto == 1 { if n5_halted == 1 { if n5_exit == ref.halt_code { n5ok = 1 } } } } 603 gv_check("the-subject-is-resumed-and-runs-to-completion-even-when-the-script-NEVER-issued-detach" as *u8, n5ok, ctr) 604 605 // ---- SESSION 6: NEG-CONTROL, NEVER-BRICK, tested by BEHAVIOUR. A pid-shaped argument must be 606 // refused: there is no path by which a running process becomes this debugger's subject. Asserted 607 // this way rather than by grepping for a word, because both this file and the organ DISCUSS 608 // ptrace, and a detector that matches its own explanation of a bug is the trap already paid for. 609 words[0] = "run" as i64 610 words[1] = "12345" as i64 611 words[2] = DG_LT as i64 612 words[3] = "continue" as i64 613 rc = dg_session(elf, words, 4, "/tmp/nx_dbg_step_gate/s6.log" as *u8, out, olen) 614 n = olen[0] 615 let n6_ref: i64 = dg_has(out, n, "DBG REFUSED reason=image-absent-or-empty" as *u8) 616 dg_p(" S6 neg-control pid-shaped subject: refused=" as *u8); dg_n(n6_ref) 617 dg_p(" rc=" as *u8); dg_n(rc); dg_p("\n" as *u8) 618 var n6ok: i64 = 0 619 if n6_ref == 1 { if rc != 0 { n6ok = 1 } } 620 gv_check("neg-control-never-brick-a-pid-shaped-argument-is-REFUSED-a-live-process-can-never-become-the-subject" as *u8, n6ok, ctr) 621 622 // ---- the source-level half of the same claim: no ptrace CALL SITE. Counted and PRINTED, so a 623 // future edit that adds one fires this rather than sliding past a boolean. 624 let ptrace_sites: i64 = gk_count(DG_SRC, "sys_ptrace(" as *u8) 625 dg_p(" source scan " as *u8); dg_p(DG_SRC) 626 dg_p(": sys_ptrace( call sites=" as *u8); dg_n(ptrace_sites); dg_p("\n" as *u8) 627 var noptrace: i64 = 0 628 if ptrace_sites == 0 { noptrace = 1 } 629 gv_check("neg-control-never-brick-the-shipped-source-contains-ZERO-ptrace-call-sites-so-no-host-process-is-reachable" as *u8, noptrace, ctr) 630 631 // ---- the budget must announce its provenance on every run, or nobody can tell a derived bound 632 // from a stale pinned one without reading the source. 633 // READ A SESSION THAT ACTUALLY ATTACHED. The first draft of this tooth asserted against session 634 // 6's buffer -- the pid-shaped refusal, which exits BEFORE any machine is built and therefore 635 // could never print a budget line. It failed for the RIGHT reason and caught itself: a tooth 636 // whose subject never reached the condition it is testing proves nothing about the condition. 637 n = gk_read("/tmp/nx_dbg_step_gate/s1.log" as *u8, out, DG_OUT_CAP - 1) 638 if n < 0 { n = 0 } 639 out[n] = 0 as u8 640 var attached: i64 = 0 641 if dg_has(out, n, "DBG image=" as *u8) == 1 { attached = 1 } 642 gv_check("provenance-tooth-precondition-the-session-being-read-really-attached-to-a-machine" as *u8, attached, ctr) 643 var prov: i64 = 0 644 if dg_has(out, n, "budget_provenance=" as *u8) == 1 { prov = 1 } 645 gv_check("every-run-announces-which-step-budget-is-in-force-and-where-it-came-from" as *u8, prov, ctr) 646 647 return gv_verdict("DBG-STEP-GATE" as *u8, ctr, "statement stepping proven on a runtime-assembled RV64 fixture over a line table emitted by nx_dwarf_line" as *u8) 648}