Skip to content

Windows JIT DLL cache: stale absolute-address bake survives across runs → AV on cache hit #2802

@borisbat

Description

@borisbat

Updated root cause (replaces the original get_version_info description)

The crash is not in get_version_info — that part of dasProfile main.das runs fine. The crash is in JIT-compiled benchmark code invoked after loading a previously-cached JIT DLL from .jitted_scripts/<ns>/<hash>.dll.

Repro (worktree daslang 0.6.2, LLVM 22.1.5, MSVC 19.44, Windows 11 26200)

  1. Run daslang -jit main.das -- --test sha256 --nomain once from D:\DASPKG\dasProfile — populates .jitted_scripts/ cache. First run reports DLL cache miss, codegen for .jitted_scripts/_anon_XXX/0xYYY.dll, runs successfully.
  2. Run the same command again. Reports DLL cache hit ...0xYYY.dll. Crashes:
CRASH: EXCEPTION_ACCESS_VIOLATION (0xC0000005) at address 0x7ffcaab54f00
  writing address 0x7ffcaab54f00
Stack trace:
  [ 0] 0x7ffcaab54f00                          ← garbage PC
  [ 1] anon_<hash>`main_0x4_block_ + 0x7b      ← lambda body inside profile() <| $() { ... }
  [ 2] das::ModuleFileAccess::isSameFileName + 0xdf52
  [ 3] das::builtin_profile + 0x123
  [ 4] anon_<hash>`main + 0x6ed
  ...

Smoking-gun observation

Run twice (no source change, JIT cache hit both times). Fault addresses across two sessions:

  • Session A: 0x7ffcaab54f00
  • Session B: 0x7ffa5eaf4f00

Different upper bits, identical ...4f00 low bits. That's the signature of ASLR moving a DLL base while the JIT-emitted machine code has an absolute target address baked in at a constant offset. Each session, Windows loads LLVM.dll (or another runtime DLL) at a fresh base; the cached .dll's code uses the OLD base + correct offset → crash on call.

Fresh codegen re-bakes against the current load base, which is why deleting .jitted_scripts/ + re-running succeeds (cache miss → codegen). The freshly-emitted DLL is then cached, and the next run with cache hit reproduces the crash.

Faulting site

Inside dict.das (the first test in the run):

profile(BENCH_RUNS, "dictionary") <| $() {
    dict(tab, src)
}

The crash is at offset 0x7b in the JIT-compiled lambda body (_0x4_block_). The lambda is being invoked from das::builtin_profile. Repeatable across:

  • AOT+JIT+INTERP run cycle (originally reported case)
  • JIT-only run (just verified — ENABLE_AOT = false, ENABLE_INTERPRETER = false)

So this isn't an AOT-to-JIT transition artifact. JIT codegen alone produces the bad cached DLL.

Likely fix candidates

  1. JIT codegen shouldn't emit raw absolute addresses of imported functions / globals. Indirect via IAT (PE import address table) so the loader fixes things up per-session.
  2. OR invalidate the cache when target-DLL load bases change — but this defeats the cache for most practical purposes since ASLR rerandomizes each session.
  3. OR disable ASLR on the JIT-emitted DLL (/DYNAMICBASE:NO link flag) so it always loads at its preferred base. Has security implications.

(1) is the principled fix; the others are workarounds.

Repro recipe summary

:: from D:\DASPKG\dasProfile (or equivalent dasProfile checkout), vcvars64 active
daslang.exe -jit main.das -- --test sha256 --nomain    :: first run, cache miss, OK
daslang.exe -jit main.das -- --test sha256 --nomain    :: second run, cache hit, CRASH

Minimal repro is just dict.das's profile() <| $() { dict(tab, src) } pattern — any benchmark sample with a JIT'd block passed to profile(). Doesn't depend on Mono, .NET, or any cross-language runner.

Workaround for users

Add rm -rf .jitted_scripts/ to the shell wrapper running daslang -jit. Forces codegen every time. Slower (~few hundred ms per JIT'd module) but reliable.

For dasProfile specifically: the parent --json invocation passes -jit to itself (gets JIT'd) and to its popen-spawned children. Both populate + reuse the same .jitted_scripts/ dir. Clearing on entry would skip the cache for the whole sweep.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions