From 9a0dbef3213ac684989dfb4cb7ff0452c51de93d Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Mon, 20 Jul 2026 06:55:36 +0200 Subject: [PATCH] fix stack-slot leaks in get-string-global, val, eval-file, and do-in Each of these read a value or an error object off the Lua stack and never popped it, so every call permanently grew the stack of a long-lived state. Copy the value out and pop, mirroring global-exists? and call-fn. val no longer leaves the Lua error object on the stack on failure; use Luax.do-in when the message is needed. --- lua.carp | 34 ++++++++++++++++++++++----------- test/lua.carp | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/lua.carp b/lua.carp index 63d3882..a772c1e 100644 --- a/lua.carp +++ b/lua.carp @@ -430,12 +430,15 @@ types, see [`Luax.set-int-global`](#set-int-global) and friends.") (do (push-carp-str lua s) (set-global lua (cstr name)))) (doc get-string-global "Fetch the global `name` and return it as a Carp -`String`. Returns an empty string if the global is nil or not a string. For a -safe version returning `Maybe`, see [`Luax.get-string-global`](#get-string-global).") +`String`. Returns an empty string if the global is nil or not a string. Pops the +global from the stack internally. For a safe version returning `Maybe`, see +[`Luax.get-string-global`](#get-string-global).") (defn get-string-global [lua name] (do (get-global lua (cstr name)) - (String.from-cstr-or (to-string lua -1) @""))) + (let-do [result (String.from-cstr-or (to-string lua -1) @"")] + (pop lua 1) + result))) (doc global-exists? "Check whether the global `name` is defined (non-nil). Pushes and pops internally, leaving the stack unchanged.") @@ -446,13 +449,16 @@ Pushes and pops internally, leaving the stack unchanged.") (doc eval-file "Load and execute the Lua file at `path`. Returns `(Success \"\")` -on success or `(Error msg)` with the Lua error message on failure.") +on success or `(Error msg)` with the Lua error message on failure. Leaves the +stack unchanged in both cases.") (sig eval-file (Fn [&Lua &String] (Result String String))) (defn eval-file [lua path] (let [res (do-file lua (cstr path))] (if (= res OK) (Result.Success @"") - (Result.Error (String.from-cstr-or (to-string lua -1) @"unknown error"))))) + (let-do [err (String.from-cstr-or (to-string lua -1) @"unknown error")] + (pop lua 1) + (Result.Error err))))) (doc table-length "Count the entries in the table at `index` by iterating with [`next`](#next). Returns -1 if the value at `index` is not a table. Leaves the @@ -469,14 +475,17 @@ stack unchanged.") count)))) (doc val "Evaluate the Lua expression `value` and assign the result to the -global `name`. Returns `OK` on success. +global `name`. Returns `OK` on success. On failure the Lua error object is +discarded, leaving the stack unchanged; use [`Luax.do-in`](#do-in) if you need +the message. ``` (Lua.val lua \"pi\" \"3.14159\") ```") (defn val [lua name value] - (let [res (Lua.do-string lua (cstr &(fmt "return %s" value)))] - (if (= res Lua.OK) (do (Lua.set-global lua (cstr name)) res) res))) + (let-do [res (Lua.do-string lua (cstr &(fmt "return %s" value)))] + (if (= res Lua.OK) (Lua.set-global lua (cstr name)) (Lua.pop lua 1)) + res)) (doc fun "Define a Lua function from a Carp macro call. `name` becomes a Lua global, `args` is a list of parameter names, and `body` is the Lua source. @@ -844,14 +853,17 @@ unchanged.") (doc do-in "Compile and execute the Lua string `code`. Returns `(Success \"\")` on success or `(Error msg)` with the Lua error message on failure. Wraps -[`Lua.do-string`](#do-string) with `Result` handling.") +[`Lua.do-string`](#do-string) with `Result` handling, leaving the stack +unchanged in both cases.") (sig do-in (Fn [&Lua &String] (Result String String))) (defn do-in [lua code] (let [res (Lua.do-string lua (cstr code))] (if (= res Lua.OK) (Result.Success @"") - (Result.Error - (String.from-cstr-or (Lua.to-string lua -1) @"unknown error"))))) + (let-do [err (String.from-cstr-or (Lua.to-string lua -1) + @"unknown error")] + (Lua.pop lua 1) + (Result.Error err))))) (doc call-fn "Call the Lua function `name` and read the result with `getter`. Arguments are passed as push expressions—the `lua` state argument is inserted diff --git a/test/lua.carp b/test/lua.carp index a12a687..d246ebd 100644 --- a/test/lua.carp +++ b/test/lua.carp @@ -383,4 +383,55 @@ (Lua.push-int lua 22) (ignore (Lua.call lua 2 1 0)) (Lua.get-int lua -1)) - "functions loaded via do-file are callable from Carp")) + "functions loaded via do-file are callable from Carp") + + (assert-equal test + 0 + (Lua.with-lua-do (Lua.libs lua) + (Lua.set-string-global lua "s" "value") + (for [i 0 30] (ignore (Lua.get-string-global lua "s"))) + (Lua.get-top lua)) + "get-string-global leaves the stack unchanged") + (assert-equal test + 0 + (Lua.with-lua-do (Lua.libs lua) + (for [i 0 30] (ignore (Lua.get-string-global lua "nope"))) + (Lua.get-top lua)) + "get-string-global leaves the stack unchanged for undefined globals") + (assert-equal test + 0 + (Lua.with-lua-do (Lua.libs lua) + (for [i 0 20] (ignore (Lua.val lua "x" "42"))) + (Lua.get-top lua)) + "val leaves the stack unchanged") + (assert-equal test + 0 + (Lua.with-lua-do (Lua.libs lua) + (for [i 0 20] (ignore (Lua.val lua "x" "bad$$syntax"))) + (Lua.get-top lua)) + "val leaves the stack unchanged after failed calls") + (assert-equal test + 0 + (Lua.with-lua-do (Lua.libs lua) + (for [i 0 20] (ignore (Lua.eval-file lua "test/test.lua"))) + (Lua.get-top lua)) + "eval-file leaves the stack unchanged") + (assert-equal test + 0 + (Lua.with-lua-do (Lua.libs lua) + (for [i 0 20] + (ignore (Lua.eval-file lua "test/nonexistent.lua"))) + (Lua.get-top lua)) + "eval-file leaves the stack unchanged after failed calls") + (assert-equal test + 0 + (Lua.with-lua-do (Lua.libs lua) + (for [i 0 20] (ignore (Luax.do-in lua "x = 42"))) + (Lua.get-top lua)) + "do-in leaves the stack unchanged") + (assert-equal test + 0 + (Lua.with-lua-do (Lua.libs lua) + (for [i 0 20] (ignore (Luax.do-in lua "bad$$syntax"))) + (Lua.get-top lua)) + "do-in leaves the stack unchanged after failed calls"))