From bdecd2b8f213bdb7806e72666892bf9ff936252b Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 13 Sep 2026 20:27:44 +0700 Subject: [PATCH] slice-from-ptr on the x86 backend, and the check that has to be signed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Another lane landed (slice-from-ptr p n) while this backend was not looking, and it arrived as two refusals rather than one: slice-from-ptr.flan and bounds.flan both stopped building through --x86. Neither is a new obstacle — a Slice _ is {ptr, i64} here exactly as it is in emit.ml, so the form is one store of the pointer and one of the length and no new representation at all. The half worth writing down is the check. There is nothing to compare the length against — only the caller knows how many elements live behind that pointer — so what is checked is that the promise is not absurd, and that test is *signed*. check_slice's own compares are unsigned, and a negative i32 sign-extended to 64 bits is a huge unsigned value that an unsigned "hi <= len" waves through; the result would be a slice about 2^64 long that reads as a pass and faults somewhere else entirely. Nothing in the corpus walks that path: every length in slice-from-ptr.flan is a literal, and a negative literal is refused by check.ml before any code is emitted. So spike/x86/p7-slice-from-ptr.flan takes the length as a parameter and runs it through a restart-case, which puts the condition's low/high/length on stdout and compares them against the LLVM build. --- lib/x86.ml | 35 ++++++++++++++++++++++++ spike/x86/p7-slice-from-ptr.flan | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 spike/x86/p7-slice-from-ptr.flan diff --git a/lib/x86.ml b/lib/x86.ml index 3ca87bf..07eba99 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -1900,6 +1900,41 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) dst = load_loc f ~reg:rcx llo lo.Tast.ty; sub_rr f.b ~dst:rax ~src:rcx; store_int f.b ~src:rax ~mm:(lmem f (shift dst 8) ~scratch:r11) ~size:8 + (* (slice-from-ptr p n): the two words a slice already is, with the pointer + the caller handed over and the length the caller promised. A [Slice _] is + {ptr, i64} here exactly as it is in [emit.ml], so there is no new + representation to build — one store of the pointer and one of the length. + + The check is the *length itself* and not a range, because nothing here + knows how many elements live behind that pointer; only the caller does. + So what is checked is the half that can be — that the promise is not + absurd — and it is a *signed* test, which matters: [check_slice]'s + compares are unsigned, and a negative i32 sign-extended to 64 bits is a + huge unsigned value that [jbe] waves straight through. + + It reuses [flan_slice_error] for [emit.ml]'s reason: the violated + condition is 0 <= n, which has the shape of a reversed slice, so the range + is reported as [0 n) against a length of 0. *) + | Tast.SliceFromPtr, [ p; n ] -> + let lp = eval f p in + let ln = eval f n in + if f.md.Emit.checks then + scoped f (fun () -> + let a = ptmp f and b = ptmp f and c = ptmp f in + xor_rr f.b ~dst:rax ~src:rax; + store_int f.b ~src:rax ~mm:(Frame a) ~size:8; + store_int f.b ~src:rax ~mm:(Frame c) ~size:8; + load_loc f ~reg:rax ln n.Tast.ty; + store_int f.b ~src:rax ~mm:(Frame b) ~size:8; + cmp_imm f.b ~dst:rax 0; + let ok = new_label f "inb" in + jcc_lbl f.b ~cc:cc_ge ok; + bounds_call f "flan_slice_error" e.Tast.loc [ a; b; c ]; + lbl f.b ok); + load_loc f ~reg:rax lp p.Tast.ty; + store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8; + load_loc f ~reg:rax ln n.Tast.ty; + store_int f.b ~src:rax ~mm:(lmem f (shift dst 8) ~scratch:r11) ~size:8 (* string and [u8] are the same two words, so both directions are views and not copies — the same non-instruction [emit.ml] emits. *) | (Tast.Bytes | Tast.StrOfBytes), [ a ] -> lower f a dst diff --git a/spike/x86/p7-slice-from-ptr.flan b/spike/x86/p7-slice-from-ptr.flan new file mode 100644 index 0000000..78889bb --- /dev/null +++ b/spike/x86/p7-slice-from-ptr.flan @@ -0,0 +1,46 @@ +;;;; (slice-from-ptr p n) with a length the checker cannot see. +;;;; +;;;; test/programs/slice-from-ptr.flan covers the form itself, but every length +;;;; in it is a literal, and a negative literal is refused by check.ml before +;;;; any code is emitted. So the run-time half of the check -- the one both +;;;; backends plant beside the form -- is walked by nothing in the corpus. +;;;; +;;;; The half that matters here is that the test is *signed*. check_slice's own +;;;; compares are unsigned, and a negative i32 sign-extended to 64 bits is a +;;;; huge unsigned value that an unsigned "hi <= len" waves straight through. +;;;; Getting that wrong yields a slice whose length is about 2^64, which reads +;;;; as a pass and segfaults somewhere else entirely. +;;;; +;;;; Both cases go through a restart-case, so what is compared is the message +;;;; on stderr as well as the fact that something was signalled. + +(defvar a [4 i32]) + +(defn promised [n i32] i32 + ;; n is a parameter, so the checker has no literal to look at. + (restart-case + (let [s (slice-from-ptr (addr (at a 0)) n)] + (len s)) + (give-up [] -1))) + +(defn show [name string n i64] () + (print name) + (print " ") + (println n)) + +(defn main [] () + (dotimes [i 4] + (set (at a i) (* (+ i 1) 10))) + + (handler-bind + [(BoundsError [c] + (show "low" (.low c)) + (show "high" (.high c)) + (show "length" (.length c)) + (invoke-restart 'give-up))] + + (println (promised 4)) ; 4 -- the truth + (println (promised 0)) ; 0 -- empty is not an error + (println (promised 2)) ; 2 -- shorter than the truth is legal + (println (promised -1)) ; -1 -- signalled, and the restart answered + (println (promised -1000000))))