build(phaseb): run ASan instrumentation in-process too

--asan used to force the legacy PhaseB because the instrumentation step was a separate
`opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false` child
process. ircore.dll now exposes ircore_mlir_to_obj_asan / ircore_llvmir_to_obj_asan, which
run the same passes through LLVM's C pass builder (LLVMRunPasses) between MLIR lowering and
codegen, so ASan builds also avoid the three child processes.

- the two cl::opt values are parsed once (std::call_once) before the first pass run
- PhaseB loads the asan entry point together with the others and calls it when _asan_mode
  is set; the legacy `opt` path stays for --phaseb-legacy and is unchanged
- --traceback (llc -frame-pointer=all) and raw-IR modules (llvm-link/opt) still take the
  legacy flow

Verified on a real VKernel module: the ircore-built object and the `opt`-instrumented one
reference the same ASan runtime symbols (69 __asan_report / 4 asan.module_ctor), and a
deliberate heap overflow is reported identically by both builds.
This commit is contained in:
TermiNexus committed 2026-10-30 20:09:00 +08:00
1 parent ef4582dcfc
commit eba9e9b6cc
1 file changed
+15 -7
+15 -7
View File
@@ -79,6 +79,7 @@ _phaseb_backend: int = PHASEB_BACKEND_IRCORE
_ircore_lib: win32.win32base.HANDLE = None
_ircore_ready: int = 0
_ircore_obj_fn: callable[[char*, sizet, char**, sizet*, char**, sizet*], int] = None
_ircore_asan_fn: callable[[char*, sizet, char**, sizet*, char**, sizet*], int] = None
_ircore_free_fn: callable[[char*], int] = None
# ircore.dll imports the exact LLVM build it was compiled with (MSYS2 clang64 22.1.x).
@@ -90,8 +91,8 @@ extern def SetDllDirectoryA(lpPathName: LPCSTR) -> BOOL
def _ircore_load() -> int:
"""Load ircore.dll and resolve its two entry points; 1 = available, 0 = not available."""
global _ircore_lib, _ircore_obj_fn, _ircore_free_fn
"""Load ircore.dll and resolve its entry points; 1 = available, 0 = not available."""
global _ircore_lib, _ircore_obj_fn, _ircore_asan_fn, _ircore_free_fn
if _ircore_lib is not None: return 1
h: win32.win32base.HANDLE = win32.win32base.LoadLibraryA("ircore.dll")
if h is None:
@@ -101,11 +102,13 @@ def _ircore_load() -> int:
if h is None:
return 0
p_obj: win32.win32base.VOIDPTR = win32.win32base.GetProcAddress(h, "ircore_mlir_to_obj")
p_asan: win32.win32base.VOIDPTR = win32.win32base.GetProcAddress(h, "ircore_mlir_to_obj_asan")
p_free: win32.win32base.VOIDPTR = win32.win32base.GetProcAddress(h, "ircore_free")
if p_obj is None or p_free is None:
if p_obj is None or p_asan is None or p_free is None:
win32.win32base.FreeLibrary(h)
return 0
_ircore_obj_fn = p_obj
_ircore_asan_fn = p_asan
_ircore_free_fn = p_free
_ircore_lib = h
return 1
@@ -545,7 +548,12 @@ def _ircore_compile_module(ir_path: str, obj_path: str, module_name: str) -> int
out_len: sizet = 0
err: char* = None
err_len: sizet = 0
rc: int = _ircore_obj_fn(src, sizet(fsize), &out_obj, &out_len, &err, &err_len)
# --asan: the DLL runs forceattrs,asan before codegen (same passes/options as the legacy
# `opt -passes=forceattrs,asan ...` step); the project still links the ASan runtime.
if _asan_mode != 0:
rc: int = _ircore_asan_fn(src, sizet(fsize), &out_obj, &out_len, &err, &err_len)
else:
rc: int = _ircore_obj_fn(src, sizet(fsize), &out_obj, &out_len, &err, &err_len)
stdlib.free(src)
if rc != 0:
@@ -583,7 +591,7 @@ def compile_ll_to_obj(ir_path: str, output_dir: str, module_name: str, cc_cmd: s
# IRCore backend: one in-process call for plain modules. ASan instrumentation, raw-IR
# merging (llvm-link/opt), --traceback frame pointers and --phase2command still need the
# external tools, so they fall through to the legacy command flow below (kept intact).
if _phaseb_ircore_active() != 0 and _asan_mode == 0 and _traceback_mode == 0 \
if _phaseb_ircore_active() != 0 and _traceback_mode == 0 \
and _phase2_cmd_path is None:
raw_sidecar: str = _ir_sibling_path(ir_path, ".raw.ll")
inline_marker: str = _ir_sibling_path(ir_path, ".inline")
@@ -832,7 +840,7 @@ def init_llc_task(task: LlcTask*, ir_path: str, output_dir: str, module_name: st
# IRCore backend: plain modules compile in-process (the commands above stay allocated for
# the legacy path / reporting but are not run). ASan instrumentation, raw-IR merging and
# --phase2command keep the external tools.
if _phaseb_ircore_active() != 0 and _asan_mode == 0 and _traceback_mode == 0 \
if _phaseb_ircore_active() != 0 and _traceback_mode == 0 \
and _phase2_cmd_path is None and has_raw == 0 and need_inline == 0:
task.UseIrcore = 1
task.IrPath = stdlib.malloc(len(ir_path) + 1)
@@ -2271,7 +2279,7 @@ def _tb_read_whole(mb: memhub.MemBuddy*, path: str) -> str:
if buf is None:
f.close()
return None
# 容量全程用 sizet(与 sz 同宽):旧写法 ULONG(sz) + ULONG(2) 会在 sz >= 4GiB 时静默回绕
# 容量全程�?sizet(与 sz 同宽):旧写�?ULONG(sz) + ULONG(2) 会在 sz >= 4GiB 时静默回�?
got: LONG = f.read_all(buf, sizet(sz) + 2)
f.close()
if got < 0: return None