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)
- 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.
- 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
- 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.
- OR invalidate the cache when target-DLL load bases change — but this defeats the cache for most practical purposes since ASLR rerandomizes each session.
- 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.
Updated root cause (replaces the original
get_version_infodescription)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)
daslang -jit main.das -- --test sha256 --nomainonce fromD:\DASPKG\dasProfile— populates.jitted_scripts/cache. First run reportsDLL cache miss, codegen for .jitted_scripts/_anon_XXX/0xYYY.dll, runs successfully.DLL cache hit ...0xYYY.dll. Crashes:Smoking-gun observation
Run twice (no source change, JIT cache hit both times). Fault addresses across two sessions:
0x7ffcaab54f000x7ffa5eaf4f00Different upper bits, identical
...4f00low 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):
The crash is at offset 0x7b in the JIT-compiled lambda body (
_0x4_block_). The lambda is being invoked fromdas::builtin_profile. Repeatable across: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
/DYNAMICBASE:NOlink 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, CRASHMinimal repro is just
dict.das'sprofile() <| $() { dict(tab, src) }pattern — any benchmark sample with a JIT'd block passed toprofile(). 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
--jsoninvocation passes-jitto 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.