Skip to content

Commit dd00fbd

Browse files
dileepr1cursoragent
authored andcommitted
fix(tests): preload the ASAN runtime for host-asan builds
The test-side ASAN handling is gated on is_asan(), which only matches BUILD_VARIANT == "asan". Under the host-asan variant it silently does nothing, so executables that are not themselves linked against the ASAN runtime abort as soon as they load an instrumented ROCm library: ASan runtime does not come first in initial library list; you should either link runtime to your application or manually preload it with LD_PRELOAD. Add is_asan_instrumented(), covering both variants, and use it for the handling that exists to cope with instrumented host libraries. is_asan() keeps its current meaning so gates that are specific to full host+device ASAN are unaffected -- notably the rocminfo skip for #3312, which passes under host-asan and should not start skipping. test_hip_printf compiles hip_check without -fsanitize=address and then runs it, so it needs the same preload. Compiling it with the sanitizer instead would pull in device-side instrumentation, which is what the host-asan variant exists to avoid. The runtime lookup was duplicated in test_hiptests.py and test_hipfile.py; both now share one implementation rather than adding a third copy. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 691329a commit dd00fbd

5 files changed

Lines changed: 123 additions & 45 deletions

File tree

build_tools/github_actions/amdgpu_family_matrix.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929

3030
import copy
3131
import os
32+
import platform
3233
import random
34+
import shlex
35+
import subprocess
3336
import sys
3437
from pathlib import Path
3538

@@ -78,6 +81,41 @@ def is_asan():
7881
return BUILD_VARIANT == "asan"
7982

8083

84+
def is_asan_instrumented():
85+
"""Determines if this build's host libraries carry ASAN instrumentation.
86+
87+
Unlike is_asan(), this covers the host-asan variant as well. Use it for
88+
anything that has to cope with instrumented host libraries; use is_asan()
89+
only to gate behavior specific to full (host + device) ASAN.
90+
"""
91+
BUILD_VARIANT = os.getenv("BUILD_VARIANT", "")
92+
return BUILD_VARIANT in ("asan", "host-asan")
93+
94+
95+
def get_asan_lib_path(bin_dir):
96+
"""Resolves the ASAN runtime shared library shipped with the build's clang.
97+
98+
Executables that are not themselves linked against the runtime must preload
99+
this, otherwise loading an instrumented ROCm library pulls the runtime in
100+
too late and it aborts with "ASan runtime does not come first in initial
101+
library list".
102+
"""
103+
arch = platform.machine()
104+
clang_path = str(Path(bin_dir).parent / "lib" / "llvm" / "bin" / "clang++")
105+
asan_lib = f"libclang_rt.asan-{arch}.so"
106+
cmd = [clang_path, f"-print-file-name={asan_lib}"]
107+
_log(f"++ Exec [{clang_path}]$ {shlex.join(cmd)}")
108+
result = subprocess.run(cmd, check=True, text=True, capture_output=True)
109+
# Clang echoes the bare filename back when it cannot resolve it.
110+
resolved = result.stdout.strip()
111+
if not resolved or resolved == asan_lib or not Path(resolved).is_file():
112+
raise FileNotFoundError(
113+
f"Could not locate ASan runtime '{asan_lib}' via {clang_path} "
114+
f"(got: '{resolved}')"
115+
)
116+
return str(Path(resolved).resolve())
117+
118+
81119
def select_weighted_label(labels_config: list[dict], context_name: str) -> str:
82120
"""Select a runner label via weighted random pick.
83121

build_tools/github_actions/test_executable_scripts/test_hipfile.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
# packaged unit suite.
2020

2121
import logging
22-
import platform
2322
import shlex
2423
import subprocess
2524
import sys
@@ -32,9 +31,9 @@
3231
SCRIPT_DIR = Path(__file__).resolve().parent
3332
THEROCK_DIR = SCRIPT_DIR.parent.parent.parent
3433

35-
# Importing is_asan from amdgpu_family_matrix.py
34+
# Importing ASAN helpers from amdgpu_family_matrix.py
3635
sys.path.append(str(THEROCK_DIR / "build_tools" / "github_actions"))
37-
from amdgpu_family_matrix import is_asan
36+
from amdgpu_family_matrix import get_asan_lib_path, is_asan_instrumented
3837

3938
if THEROCK_BIN_DIR is None:
4039
logging.error("env(THEROCK_BIN_DIR) is not set. Set it before running tests.")
@@ -48,25 +47,9 @@
4847
raise SystemExit(1)
4948

5049

51-
def get_asan_lib_path():
52-
arch = platform.machine()
53-
clang_path = str(Path(THEROCK_BIN_DIR).parent / "lib" / "llvm" / "bin" / "clang++")
54-
asan_lib = f"libclang_rt.asan-{arch}.so"
55-
cmd = [clang_path, f"-print-file-name={asan_lib}"]
56-
logging.info(f"++ Exec [{clang_path}]$ {shlex.join(cmd)}")
57-
result = subprocess.run(cmd, check=True, text=True, capture_output=True)
58-
resolved = result.stdout.strip()
59-
if not resolved or resolved == asan_lib or not Path(resolved).is_file():
60-
raise FileNotFoundError(
61-
f"Could not locate ASan runtime '{asan_lib}' via {clang_path} "
62-
f"(got: '{resolved}')"
63-
)
64-
return str(Path(resolved).resolve())
65-
66-
6750
env = os.environ.copy()
68-
if is_asan():
69-
asan_lib = get_asan_lib_path()
51+
if is_asan_instrumented():
52+
asan_lib = get_asan_lib_path(THEROCK_BIN_DIR)
7053
existing_preload = env.get("LD_PRELOAD", "")
7154
env["LD_PRELOAD"] = (
7255
f"{existing_preload}:{asan_lib}" if existing_preload else asan_lib

build_tools/github_actions/test_executable_scripts/test_hiptests.py

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
os_type = platform.system().lower()
2525
CATCH_TESTS_PATH = str(Path(THEROCK_BIN_DIR).parent / "share" / "hip" / "catch_tests")
2626

27-
# Importing is_asan from amdgpu_family_matrix.py
27+
# Importing ASAN helpers from amdgpu_family_matrix.py
2828
sys.path.append(str(THEROCK_DIR / "build_tools" / "github_actions"))
29-
from amdgpu_family_matrix import is_asan
29+
from amdgpu_family_matrix import get_asan_lib_path, is_asan_instrumented
3030

3131
env = os.environ.copy()
3232

@@ -107,20 +107,6 @@
107107
]
108108

109109

110-
def get_asan_lib_path():
111-
arch = platform.machine()
112-
CLANG_PATH = str(Path(THEROCK_BIN_DIR).parent / "lib" / "llvm" / "bin" / "clang++")
113-
cmd = [f"{CLANG_PATH}", f"--print-file-name=libclang_rt.asan-{arch}.so"]
114-
logging.info(f"++ Exec [{CLANG_PATH}]$ {shlex.join(cmd)}")
115-
result = subprocess.run(
116-
cmd,
117-
check=True,
118-
text=True,
119-
capture_output=True,
120-
)
121-
return result.stdout.strip()
122-
123-
124110
def copy_dlls_exe_path():
125111
if platform.system() == "Windows":
126112
# hip and comgr dlls need to be copied to the same folder as exectuable
@@ -161,8 +147,8 @@ def setup_env(env):
161147
else:
162148
env["LD_LIBRARY_PATH"] = HIP_LIB_PATH
163149
# For ASAN mode, we preload it for test count query and test running
164-
if is_asan():
165-
env["LD_PRELOAD"] = get_asan_lib_path()
150+
if is_asan_instrumented():
151+
env["LD_PRELOAD"] = get_asan_lib_path(THEROCK_BIN_DIR)
166152
env["HSA_XNACK"] = "1"
167153
# Increase stack size of clr threads
168154
env["CQ_THREAD_STACK_SIZE"] = "8388608"
@@ -178,7 +164,7 @@ def setup_env(env):
178164

179165
def execute_tests(env):
180166
# Allow for more time in ASAN mode to run the tests.
181-
timeout = 1500 if is_asan() else 600
167+
timeout = 1500 if is_asan_instrumented() else 600
182168
cmd = [
183169
"ctest",
184170
"--tests-information",

build_tools/github_actions/tests/amdgpu_family_matrix_test.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
import amdgpu_family_matrix
2121
from amdgpu_family_matrix import (
2222
get_all_families_for_trigger_types,
23+
get_asan_lib_path,
2324
get_build_runner_labels,
25+
is_asan,
26+
is_asan_instrumented,
2427
load_external_runner_config,
2528
)
2629

@@ -277,5 +280,63 @@ def test_v1_external_config_extracts_runner_labels(self):
277280
self.assertIn("asan", result["gfx94x"]["linux"]["build_variants"])
278281

279282

283+
class AsanHelpersTest(unittest.TestCase):
284+
def test_is_asan_only_matches_full_asan(self):
285+
"""is_asan() gates full (host + device) ASAN behavior only."""
286+
for variant, expected in [
287+
("asan", True),
288+
("host-asan", False),
289+
("release", False),
290+
("tsan", False),
291+
("", False),
292+
]:
293+
with mock.patch.dict(os.environ, {"BUILD_VARIANT": variant}):
294+
self.assertEqual(is_asan(), expected, f"BUILD_VARIANT={variant}")
295+
296+
def test_is_asan_instrumented_covers_host_asan(self):
297+
"""Host libraries are instrumented under both ASAN variants."""
298+
for variant, expected in [
299+
("asan", True),
300+
("host-asan", True),
301+
("release", False),
302+
("tsan", False),
303+
("", False),
304+
]:
305+
with mock.patch.dict(os.environ, {"BUILD_VARIANT": variant}):
306+
self.assertEqual(
307+
is_asan_instrumented(), expected, f"BUILD_VARIANT={variant}"
308+
)
309+
310+
def test_is_asan_instrumented_without_build_variant_set(self):
311+
"""A missing BUILD_VARIANT must not be treated as an ASAN build."""
312+
with mock.patch.dict(os.environ, clear=False) as _:
313+
os.environ.pop("BUILD_VARIANT", None)
314+
self.assertFalse(is_asan_instrumented())
315+
316+
def test_get_asan_lib_path_returns_resolved_runtime(self):
317+
with mock.patch.object(amdgpu_family_matrix.subprocess, "run") as fake_run:
318+
fake_run.return_value = mock.Mock(stdout=f"{__file__}\n")
319+
self.assertEqual(get_asan_lib_path("/rocm/bin"), str(Path(__file__)))
320+
# The runtime is looked up via the clang shipped alongside the build.
321+
cmd = fake_run.call_args[0][0]
322+
self.assertTrue(cmd[0].endswith(os.path.join("lib", "llvm", "bin", "clang++")))
323+
self.assertTrue(cmd[1].startswith("-print-file-name=libclang_rt.asan-"))
324+
325+
def test_get_asan_lib_path_raises_when_unresolved(self):
326+
"""Clang echoes the bare filename back when it cannot resolve it."""
327+
with mock.patch.object(amdgpu_family_matrix.subprocess, "run") as fake_run:
328+
fake_run.return_value = mock.Mock(
329+
stdout=f"libclang_rt.asan-{amdgpu_family_matrix.platform.machine()}.so\n"
330+
)
331+
with self.assertRaises(FileNotFoundError):
332+
get_asan_lib_path("/rocm/bin")
333+
334+
def test_get_asan_lib_path_raises_on_missing_file(self):
335+
with mock.patch.object(amdgpu_family_matrix.subprocess, "run") as fake_run:
336+
fake_run.return_value = mock.Mock(stdout="/nonexistent/libclang_rt.so\n")
337+
with self.assertRaises(FileNotFoundError):
338+
get_asan_lib_path("/rocm/bin")
339+
340+
280341
if __name__ == "__main__":
281342
unittest.main()

tests/test_rocm_sanity.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,19 @@
2020

2121
AMDGPU_FAMILIES = os.getenv("AMDGPU_FAMILIES")
2222

23-
# Importing is_asan from amdgpu_family_matrix.py
23+
# Importing ASAN helpers from amdgpu_family_matrix.py
2424
sys.path.append(str(THIS_DIR.parent / "build_tools" / "github_actions"))
25-
from amdgpu_family_matrix import is_asan
25+
from amdgpu_family_matrix import get_asan_lib_path, is_asan, is_asan_instrumented
2626

2727

2828
def is_windows():
2929
return "windows" == platform.system().lower()
3030

3131

32-
def run_command(command: list[str], cwd=None):
32+
def run_command(command: list[str], cwd=None, env=None):
3333
logger.info(f"++ Run [{cwd}]$ {shlex.join(command)}")
3434
process = subprocess.run(
35-
command, capture_output=True, cwd=cwd, shell=is_windows(), text=True
35+
command, capture_output=True, cwd=cwd, env=env, shell=is_windows(), text=True
3636
)
3737
if process.returncode != 0:
3838
logger.error(f"Command failed!")
@@ -149,7 +149,17 @@ def test_hip_printf(self):
149149
# Running and checking the executable
150150
platform_executable_prefix = "./" if not is_windows() else ""
151151
hip_check_executable = f"{platform_executable_prefix}hip_check"
152-
process = run_command([hip_check_executable], cwd=str(THEROCK_BIN_DIR))
152+
# hip_check is deliberately compiled without -fsanitize=address, so it
153+
# needs the runtime preloaded to link against instrumented ROCm
154+
# libraries. Instrumenting it instead would pull in device-side
155+
# instrumentation that the host-asan variant exists to avoid.
156+
env = None
157+
if is_asan_instrumented():
158+
env = {
159+
**os.environ,
160+
"LD_PRELOAD": get_asan_lib_path(THEROCK_BIN_DIR),
161+
}
162+
process = run_command([hip_check_executable], cwd=str(THEROCK_BIN_DIR), env=env)
153163
check.equal(process.returncode, 0)
154164
check.greater(
155165
os.path.getsize(str(THEROCK_BIN_DIR / hip_check_executable_file)), 0

0 commit comments

Comments
 (0)