From 58b1f49cf248cc3c55eb6d273c6ad58d0eb18ad3 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 13 Sep 2026 15:08:38 +0700 Subject: [PATCH] A discarded value was being stored over the return address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit edn.flan crashed by jumping into .rodata, several statements after the mistake, and the assembly at the jump read correctly. Item 15 said this is how hand-encoding fails, and it is: the crash and the cause were in different functions. The cause is one line of design. A form whose value is thrown away was handed the sink, and the sink was spelled as an address — rbp+0. That is the saved rbp, and rbp+8 is the return address, so a non-void form in statement position stored its value straight over both. A 16-byte slice did it in one rep movsb. The sink is now compared by identity and never used as an address: anything with a value that is handed it gets a frame temporary instead, reclaimed immediately. The point is not the temporary, it is that the store has somewhere legal to go. edn.flan matches the LLVM build now — 60 lines of a hand-written EDN reader, unions, options, nested collections and all. --- lib/x86.ml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/x86.ml b/lib/x86.ml index acad01b..fd5135d 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -741,12 +741,29 @@ let emit_args f (args : arg list) = (* ── Lowering ────────────────────────────────────────────────────────── *) -(* The destination a void expression is handed and never reads. rbp+0 is the - saved rbp; nothing below stores through a destination it was told was - void, so the address is a name and not a target. *) +(* The destination handed to an expression whose value is thrown away. + + It is one allocated value and it is compared by identity, because it is not + an address and must never be used as one: rbp+0 is the saved rbp and rbp+8 + is the return address, so a 16-byte slice stored "into the sink" overwrites + both and the function returns into whatever the first two words of the + value happened to be. That is not hypothetical — it is how [edn.flan] + failed, by jumping into .rodata several statements after the real mistake, + and the mistake was a form of non-void type written in statement position. + + So [lower] refuses the sink for anything that has a value, and spends a + frame temporary on it instead. The temporary is reclaimed at once; the + point is that the store has somewhere legal to go. *) let sink = Lf 0 let rec lower f (e : Tast.expr) (dst : loc) : unit = + if dst == sink && not (is_void e.Tast.ty) then + scoped f (fun () -> + let o = tmp f e.Tast.ty in + lower f e (Lf o)) + else lower_at f e dst + +and lower_at f (e : Tast.expr) (dst : loc) : unit = let t = e.Tast.ty in match e.Tast.e with | Tast.Int (n, _) -> imm_into f ~reg:rax n; store_loc f ~reg:rax dst t