diff options
| -rw-r--r-- | TODO.md | 2 | ||||
| -rw-r--r-- | src/runtime/rt.zc | 27 | ||||
| -rw-r--r-- | tests/programs/memdebug_interior.expected | 1 | ||||
| -rw-r--r-- | tests/programs/memdebug_interior.zc | 18 | ||||
| -rwxr-xr-x | tests/test.sh | 1 |
5 files changed, 39 insertions, 10 deletions
@@ -494,7 +494,7 @@ brief someone without re-reading the code. - [ ] borrow returns tied to an argument. inference already works for a callee defined before the call: `char* skip(char* s) { ...; return s + i; }` makes `let r = skip(t); rt.free(t); r[0]` an error. the gap: when the call comes before the callee's definition, nothing is reported and the program reads freed memory. extend the deferred effect requirements (`effect_require`, `effect_finish` at unit end) so a forward-called function whose return turns out to alias a parameter produces the same error. for bodyless declarations, externs and function types, add an unobtrusive spelling, for example a `borrows s` clause after the parameter list (`char* skip(char* s) borrows s;`), checked against the body when one exists. - [ ] variadic arguments and `std.print`. design: a pack is passed as one hidden argument, a caller-built read-only growable-array handle (`fixed T[?]` view: [len, cap, items, element size] in the caller's frame, so `len(a)` and `a[i]` work unchanged and nothing is allocated). `...int a` is a typed pack; several packs per function when their element types differ (`void f(...int a, ...char b)`): each extra argument goes to the pack whose element type matches its static type, an ambiguous match is an error, packs come after the fixed parameters. `...any args` packs each value with a kind code (int, float, char, string, bstr, pointer, ...) and its type record: `any` is a small built-in record { kind, value, type }. `...*any args` makes the call comma-free: after the fixed arguments, the parser reads unary expressions until `)`, commas optional, so `std.print("x = " name ", age " age "\n");` works and binary expressions must be parenthesized. `std.print(...*any args)` prints each argument by its kind (`put_n`, the float printer, `put_s`, `put_bs`, a character for `char`, hex for pointers). - [ ] x86-64 jit phase 2: compile o3 regions to x86-64 (port `rcompile` and the region register allocation; regions now run on the region interpreter and are 2 to 4 times slower than their plain versions on this jit) and inline enhancements (lower the optimized micro-ops to x86 registers as the arm64 path does, instead of calling `xrun`). validate with `tests/drivers/jit_x64_test.sh` and `tests/bench/bench.sh`, which now has an x86-64 jit column. add the driver to `tests/test.sh` on x86-64 linux. -- [ ] `memdebug`'s `obj_base` searches only 64 words back for a block header, so under `memdebug on` an interior access more than 64 words into a block (`buf[255]` on a 256-word block) reports "bad heap pointer". find the header from the buddy allocator's size classes instead of a bounded scan, with a regression program. +- [x] `memdebug`'s `obj_base` used to search only 64 words back for a block header, so `buf[255]` on a 256-word block reported "bad heap pointer". it now rounds the pointer down to each size class's alignment (largest first) and takes the first header whose check word, size and class agree and whose payload holds the pointer, so interiors at any depth work, a deep pointer into a freed block reports use after free, and header words and free space still report "bad heap pointer". regression: `tests/programs/memdebug_interior.zc` (in `tests/test.sh` beside the other memdebug checks). note that a stray pointer that lands inside another live block is accepted, since memdebug cannot know which block it came from. - [ ] (partly done, see "agent work at the end of the session") `defer` follow-ups (small): accept `defer if (...) ...;` and other single statements besides an expression; decide whether deferred code may name an owner declared after the `defer` (now rejected by the lexical rule; one line in `defer_one`). - [ ] build the llvm core by default when `llvm-config` is available (the makefile falls back to the interpreter otherwise), so x86 hosts get a native tier and the language server stops being interpreted; keep `ZYNOLLVM=1` and `-DZYINTERP` as the opt-outs. - [ ] stack-effect checking: every function and `asm` block declares or is inferred to have a fixed data-stack effect, and the compiler reports a block whose paths leave the stack unbalanced. the first step of a proving scaffold. diff --git a/src/runtime/rt.zc b/src/runtime/rt.zc index 4e52cb5..b468f2a 100644 --- a/src/runtime/rt.zc +++ b/src/runtime/rt.zc @@ -1315,7 +1315,7 @@ int scrub() { // dbg bits: 1 header checks and allocation sites, 2 poison the payload on free // (the word 57005, which is 0xDEAD), 4 interior pointers keep a quarantined -// block (the mark walks back at most 64 words to a real header). off unless +// block (obj_base finds its header from the size classes). off unless // a program requests. sites are p ^ MAGIC so the collector does not treat them as pointers. const int NSITE = 64; @@ -1324,17 +1324,26 @@ int site_drop; word[64] site_p; word[64] site_line; // true when p is a freed block still held in quarantine. word quar(word p) { return p >= HEAP_LO + HDR && p < hbump && p[-3] == (p ^ MAGIC) && !(p[-4] & 1); } -// the object whose payload contains w, or 0. exact pointers and interiors. +// the object whose payload contains w, or 0. exact pointers and interiors, at any +// depth. every block of class k starts at HEAP_LO plus a multiple of 2^k words, so +// the block holding w, if any, starts at w rounded down to one of those. the +// candidates are tried from the largest class down: a header counts only when its +// check word, size and class agree with k, and the first that holds w in its +// payload is the block. a merge clears the upper half's header and a split rewrites +// the header at the same address, so a stale header can only sit inside the +// payload of the real block, which is found first. word obj_base(int w) { - int i = 0; word p; int n; + int k = 31, off, sz; word b, p; if (w < HEAP_LO + HDR || w >= hbump) return 0; - while (i <= 64) { - p = w - i; - if (p >= HEAP_LO + HDR && p < hbump && p[-3] == (p ^ MAGIC)) { - n = p[-5] - HDR; - if (w >= p && w < p + n) return p; + off = w - HEAP_LO; + while (k >= 3) { + sz = pow2[k]; + b = HEAP_LO + off - off % sz; + p = b + HDR; + if (sz <= hbump - HEAP_LO && p < hbump && b[0] == sz && p[-3] == (p ^ MAGIC) && bclass(b) == k) { + if (w >= p && w < b + sz) return p; } - i++; + k--; } return 0; } diff --git a/tests/programs/memdebug_interior.expected b/tests/programs/memdebug_interior.expected new file mode 100644 index 0000000..580692a --- /dev/null +++ b/tests/programs/memdebug_interior.expected @@ -0,0 +1 @@ +7 3 2 diff --git a/tests/programs/memdebug_interior.zc b/tests/programs/memdebug_interior.zc new file mode 100644 index 0000000..8464f2c --- /dev/null +++ b/tests/programs/memdebug_interior.zc @@ -0,0 +1,18 @@ +namespace; +include rt; + +// memdebug finds the block of an interior pointer however deep it is: buf[255] +// of a 256-word block is 255 words past its header, beyond the old 64-word scan. +memdebug on; +fn main() { + let buf = rt.new(256), small = rt.new(4); + buf[255] = 7; + buf[100] = 3; + small[3] = 2; + rt.put_n(buf[255]); rt.put_c(32); + rt.put_n(buf[100]); rt.put_c(32); + rt.put_n(small[3]); rt.put_c(10); + rt.free(small); + rt.free(buf); + return 0; +} diff --git a/tests/test.sh b/tests/test.sh index 37fa71c..a4e58e8 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -188,6 +188,7 @@ ABC" "$l o3 lifts byte loads and stores" chk "$($vm < tests/programs/strings.zc)" "hi! 3 3 104 i! 1 1 1 -42 42 -42 11 1 -9223372036854775808 0 1 3 10 1 1 1 W ab" "$l byte strings" chk "$($vm < tests/programs/share.zc)" "1 2 1 4 1 1 1 1 42 1 1 1 6 0" "$l shared cons cells" chk "$($vm < tests/programs/share_dbg.zc)" "7 1" "$l memdebug accepts a shared cell" + chk "$($vm < tests/programs/memdebug_interior.zc | md5sum)" "$(md5sum < tests/programs/memdebug_interior.expected)" "$l memdebug finds the block of a deep interior pointer" chk "$($vm < tests/programs/global_transfer.zc)" "5 9" "$l an m or u owner stored in a global is transferred" chk "$($vm < tests/programs/closure_result.zc)" "8 1.500000" "$l a closure with a declared result type" chk "$($vm < examples/strings.zc)" "hello, zc |
