fix(jit): grow the string pool without sliding FARGS (#2107)

#2066 expanded STR by moving FARGS from 0x4000-0x8000 up to
0x9000-0xD000.  That was enough to SIGSEGV nested Lua-in-softcode JIT
on at least one host while Linux/x86_64 stayed green — the expression
that crashed is tiny, so the enlarged capacity was not the trigger;
the layout move was.

Keep FARGS at its historical window and put the 32 KB (= LBUF) string
pool in the free gap before the blob:

  CODE   0x0000-0x1000
  (gap)  0x1000-0x4000   (former 12 KB STR; unused)
  FARGS  0x4000-0x8000   (unmoved)
  STR    0x8000-0x10000  (32 KB)
  BLOB   0x10000+

Also fold the layout constants into s_blob_version so any future map
change invalidates the persisted code_cache even when __DATE__/__TIME__
does not move, and fix the legacy single-blob reconstruct high-water
(was FARGS_LIMIT; STR is now above it).  Drop the dead OUT_BASE constant.
This commit is contained in:
Stephen Dennis 2026-08-05 16:25:56 +00:00
parent 5845a81f14
commit 43a4eb094e
3 changed files with 61 additions and 26 deletions

View file

@ -284,27 +284,38 @@ struct rv_compiler {
static constexpr size_t MEM_SIZE = 0x540000; // 4 MB map + 1 MB heap + 256 KB doubles scratch
static constexpr uint64_t CODE_BASE = 0x0000;
static constexpr uint64_t CODE_LIMIT = 0x1000;
// String pool sized to hold one full LBUF (32768) of constants (#2066).
// Previously STR_LIMIT was 0x4000 (12 KB) while OUT_SLOT tracked LBUF at
// 32 KB, so any expression whose pooled constants exceeded ~12 KB fell
// back to the AST evaluator with COMPILATION FAILED. FARGS moved up to
// keep the same 16 KB capacity without overlapping the string region.
// Cached programs from the old layout fail the fargs-range check on load
// and recompile cleanly.
static constexpr uint64_t STR_BASE = 0x1000;
static constexpr uint64_t STR_LIMIT = 0x9000; // STR_BASE + 0x8000 (= LBUF_SIZE)
static constexpr uint64_t FARGS_BASE = 0x9000;
static constexpr uint64_t FARGS_LIMIT= 0xD000; // FARGS_BASE + 0x4000
// Guest one-shot layout (#2066, #2107):
//
// CODE 0x0000 .. 0x1000 (4 KB)
// (gap) 0x1000 .. 0x4000 (was the 12 KB string pool; unused)
// FARGS 0x4000 .. 0x8000 (16 KB) — UNMOVED from pre-#2066
// STR 0x8000 .. 0x10000 (32 KB = LBUF_SIZE)
// BLOB 0x10000 ..
//
// #2066 first grew STR by sliding FARGS up (STR 0x1000-0x9000, FARGS
// 0x9000-0xD000). That was enough to make nested Lua-in-softcode JIT
// SIGSEGV on at least one host (#2107) while this box stayed green —
// so FARGS stays at its historical window and the enlarged string
// pool takes the free gap that used to sit under the legacy OUT_BASE
// and before the blob. 0x1000-0x4000 is left empty on purpose: a
// contiguous STR bump allocator cannot share it with a fixed FARGS.
//
// Cached programs from any prior layout fail the range checks on
// load (and the blob_version now includes these constants, #2107).
static constexpr uint64_t FARGS_BASE = 0x4000;
static constexpr uint64_t FARGS_LIMIT= 0x8000;
static constexpr uint64_t STR_BASE = 0x8000;
static constexpr uint64_t STR_LIMIT = 0x10000; // STR_BASE + 0x8000 (= LBUF_SIZE)
static constexpr uint64_t BLOB_BASE = 0x10000;
static constexpr uint64_t BLOB_LIMIT = 0x40000;
static_assert(CODE_LIMIT == STR_BASE,
"code region must abut the string pool");
static_assert(STR_LIMIT == FARGS_BASE,
"string pool must abut the fargs pool");
static_assert(FARGS_LIMIT <= BLOB_BASE,
"fargs pool must not overlap the blob region");
static_assert(CODE_LIMIT <= FARGS_BASE,
"code region must not overlap the fargs pool");
static_assert(FARGS_LIMIT == STR_BASE,
"fargs pool must abut the string pool");
static_assert(STR_LIMIT == BLOB_BASE,
"string pool must abut the blob region");
static_assert(STR_LIMIT - STR_BASE >= 32768,
"string pool must hold one LBUF of constants (#2066)");
@ -314,10 +325,9 @@ struct rv_compiler {
// frame size. Each slot is STACK_TOP - 8 - (N+1)*OUT_SLOT.
// Addresses are compile-time constants (STACK_TOP is fixed).
//
// Legacy OUT_BASE retained for backward compatibility with
// cached programs and output clearing.
// OUT_BASE was a legacy fixed output base at 0x8000; the stack
// allocator superseded it and nothing reads the constant (#2107).
//
static constexpr uint64_t OUT_BASE = 0x8000; // legacy (unused by new alloc)
static constexpr int OUT_SLOT = 32768; // = LBUF_SIZE
static constexpr uint64_t OUT_STACK_LIMIT = 0x81000; // lowest valid output addr
// Bit 30, not bit 31: RV64 LUI sign-extends bit 31 to 64 bits,

View file

@ -528,6 +528,16 @@ static bool tier2_load(const char *path, uint64_t guest_base) {
// Deliberately excludes the dbt_* units: they execute the stored RV64
// rather than produce it, so invalidating on their account would discard
// the cache for no gain. See jit_tier1_stamp.h.
// Layout constants are part of the key (#2107): a guest-address
// map change (STR/FARGS bases) rewrites every baked pointer in the
// code and pool blobs, and must not reuse a row written under the
// previous map even when __DATE__/__TIME__ happened not to move.
static const uint64_t layout[] = {
rv_compiler::CODE_BASE, rv_compiler::CODE_LIMIT,
rv_compiler::STR_BASE, rv_compiler::STR_LIMIT,
rv_compiler::FARGS_BASE, rv_compiler::FARGS_LIMIT,
rv_compiler::BLOB_BASE, rv_compiler::MEM_SIZE,
};
const void *parts[] = {
JIT_COMPILER_VERSION,
JIT_BUILD_STAMP,
@ -539,6 +549,7 @@ static bool tier2_load(const char *path, uint64_t guest_base) {
&hdr,
s_tier2.image.data(),
entries.empty() ? nullptr : entries.data(),
layout,
};
const size_t sizes[] = {
sizeof(JIT_COMPILER_VERSION) - 1,
@ -552,8 +563,9 @@ static bool tier2_load(const char *path, uint64_t guest_base) {
sizeof(hdr),
s_tier2.code_size,
entries.size() * sizeof(rv64_blob_entry),
sizeof(layout),
};
s_blob_version = sha1_hex_parts(parts, sizes, 10);
s_blob_version = sha1_hex_parts(parts, sizes, 11);
return true;
}
@ -891,6 +903,13 @@ static void tier2_lazy_init() {
// Still hash the compiler version so tier 1 upgrades invalidate. Same
// per-unit stamps as the loaded path (#2061) -- without them this leg has
// the identical hole, and it is the leg a blob-less build runs on.
// Layout is in the key for the same reason as the blob-loaded path (#2107).
static const uint64_t layout[] = {
rv_compiler::CODE_BASE, rv_compiler::CODE_LIMIT,
rv_compiler::STR_BASE, rv_compiler::STR_LIMIT,
rv_compiler::FARGS_BASE, rv_compiler::FARGS_LIMIT,
rv_compiler::BLOB_BASE, rv_compiler::MEM_SIZE,
};
const void *parts[] = {
JIT_COMPILER_VERSION,
JIT_BUILD_STAMP,
@ -899,6 +918,7 @@ static void tier2_lazy_init() {
TIER1_STAMP_HIR_SSA,
TIER1_STAMP_HIR_OPT,
TIER1_STAMP_HIR_CODEGEN,
layout,
};
const size_t sizes[] = {
sizeof(JIT_COMPILER_VERSION) - 1,
@ -908,8 +928,9 @@ static void tier2_lazy_init() {
strlen(TIER1_STAMP_HIR_SSA),
strlen(TIER1_STAMP_HIR_OPT),
strlen(TIER1_STAMP_HIR_CODEGEN),
sizeof(layout),
};
s_blob_version = sha1_hex_parts(parts, sizes, 7);
s_blob_version = sha1_hex_parts(parts, sizes, 8);
}
void tier2_ensure(void) {
@ -1986,9 +2007,13 @@ static compiled_program reconstruct_from_cache(
} else if (rec.memory_blob && rec.memory_len > 0) {
// Legacy format: single memory blob covering code+str+fargs.
// Extract the occupied regions into compact vectors.
// Cap at the highest of the three pool ends (#2107: STR now sits
// above FARGS, so FARGS_LIMIT is no longer the image high water).
int copy_len = rec.memory_len;
if (copy_len > static_cast<int>(rv_compiler::FARGS_LIMIT)) {
copy_len = static_cast<int>(rv_compiler::FARGS_LIMIT);
const int layout_end = static_cast<int>(
(std::max)(rv_compiler::STR_LIMIT, rv_compiler::FARGS_LIMIT));
if (copy_len > layout_end) {
copy_len = layout_end;
}
const auto *base = static_cast<const uint8_t *>(rec.memory_blob);

View file

@ -4,8 +4,8 @@
# A JIT-compiled expression that evaluates another expression (u(), ulocal(),
# or any outer construct wrapping one) runs the inner compile on the shared
# heap, whose string pool lives at 0x40000..0x60000 rather than the one-shot
# compiler's 0x1000..0x9000 (#2066). hir_codegen's FP slot allocator bounded
# itself against the static rv_compiler::STR_LIMIT instead of this compile's
# compiler's 0x8000..0x10000 (#2066/#2107). hir_codegen's FP slot allocator
# bounded itself against the static rv_compiler::STR_LIMIT instead of this
# own str_pool_limit, so every FP slot address stayed 0 and all float
# temporaries aliased guest address 0 (#1159).
#