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))))