Tests: Add TSan and failing multi-threaded data race test
PR #7902 uncovered a pre-existing bug in multi-threaded scheduling, where we can end up with an un-ordered R-W hazard in the MTask graph, resulting in non-deterministic runtime behaviour. This is extremely hard to actually trigger on a small example, so using ThreadSanitizer to flag it, which can identify the race reliably. In this patch: - Add configure and `verilator --get-supported TSAN` to check if the configured compiler supports ThreadSanitizer - Add a --tsan option to the test driver.py which builds the test with thread sanitizer (similar idea to --gdbsim). - Add a tests.enable_tsan() method to allow turning on TSan in the test Python file. - Add a suppressions file that waives TSan errors in the runtime library - Finally add `t_sched_hybrid_hazard` that demonstrates the data race triggered after #7902. This is currently expected failing, fix later. With the suppression, there are 17 vltmt tests failing due races in the generated code. (Using `driver.py --vltmt --tsan --quiet -j0`)
This commit is contained in:
parent
1d42c45a34
commit
f38c6aaddb
|
|
@ -507,6 +507,11 @@ _MY_CXX_CHECK_CORO_SET(CFG_CXXFLAGS_COROUTINES,-std=gnu++20)
|
||||||
AC_SUBST(CFG_CXXFLAGS_COROUTINES)
|
AC_SUBST(CFG_CXXFLAGS_COROUTINES)
|
||||||
AC_SUBST(HAVE_COROUTINES)
|
AC_SUBST(HAVE_COROUTINES)
|
||||||
|
|
||||||
|
# Check if the C++ compiler supports ThreadSanitizer
|
||||||
|
_MY_CXX_CHECK_IFELSE(-fsanitize=thread,
|
||||||
|
[AC_DEFINE([HAVE_TSAN],[1],[Defined if ThreadSanitizer is supported by $CXX])])
|
||||||
|
AC_SUBST(HAVE_TSAN)
|
||||||
|
|
||||||
# Flags for compiling Verilator internals including parser always
|
# Flags for compiling Verilator internals including parser always
|
||||||
if test "$CFG_WITH_DEV_ASAN" = "yes"; then
|
if test "$CFG_WITH_DEV_ASAN" = "yes"; then
|
||||||
_MY_CXX_CHECK_IFELSE(-fsanitize=address -DVL_ASAN,
|
_MY_CXX_CHECK_IFELSE(-fsanitize=address -DVL_ASAN,
|
||||||
|
|
|
||||||
|
|
@ -936,7 +936,7 @@ Summary:
|
||||||
:file:`*.mk` files.
|
:file:`*.mk` files.
|
||||||
|
|
||||||
Feature may be one of the following: COROUTINES, DEV_ASAN, DEV_GCOV,
|
Feature may be one of the following: COROUTINES, DEV_ASAN, DEV_GCOV,
|
||||||
SYSTEMC.
|
SYSTEMC, TSAN.
|
||||||
|
|
||||||
.. option:: --getenv <variable>
|
.. option:: --getenv <variable>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -887,6 +887,8 @@ string V3Options::getSupported(const string& var) {
|
||||||
// If update below, also update V3Options::showVersion()
|
// If update below, also update V3Options::showVersion()
|
||||||
if (var == "COROUTINES" && coroutineSupport()) {
|
if (var == "COROUTINES" && coroutineSupport()) {
|
||||||
return "1";
|
return "1";
|
||||||
|
} else if (var == "TSAN" && tsanSupport()) {
|
||||||
|
return "1";
|
||||||
} else if (var == "DEV_ASAN" && devAsan()) {
|
} else if (var == "DEV_ASAN" && devAsan()) {
|
||||||
return "1";
|
return "1";
|
||||||
} else if (var == "DEV_GCOV" && devGcov()) {
|
} else if (var == "DEV_GCOV" && devGcov()) {
|
||||||
|
|
@ -936,6 +938,14 @@ bool V3Options::devGcov() {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool V3Options::tsanSupport() {
|
||||||
|
#ifdef HAVE_TSAN
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
//######################################################################
|
//######################################################################
|
||||||
// V3 Options notification methods
|
// V3 Options notification methods
|
||||||
|
|
||||||
|
|
@ -2285,6 +2295,7 @@ void V3Options::showVersion(bool verbose) {
|
||||||
cout << "Supported features (compiled-in or forced by environment):\n";
|
cout << "Supported features (compiled-in or forced by environment):\n";
|
||||||
cout << " COROUTINES = " << getSupported("COROUTINES") << "\n";
|
cout << " COROUTINES = " << getSupported("COROUTINES") << "\n";
|
||||||
cout << " SYSTEMC = " << getSupported("SYSTEMC") << "\n";
|
cout << " SYSTEMC = " << getSupported("SYSTEMC") << "\n";
|
||||||
|
cout << " TSAN = " << getSupported("TSAN") << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
//======================================================================
|
//======================================================================
|
||||||
|
|
|
||||||
|
|
@ -830,9 +830,10 @@ public:
|
||||||
static string getSupported(const string& var);
|
static string getSupported(const string& var);
|
||||||
static bool systemCSystemWide();
|
static bool systemCSystemWide();
|
||||||
static bool systemCFound(); // SystemC installed, or environment points to it
|
static bool systemCFound(); // SystemC installed, or environment points to it
|
||||||
static bool coroutineSupport(); // Compiler supports coroutines
|
static bool coroutineSupport(); // Configured C++ compiler supports coroutines
|
||||||
static bool devAsan(); // Compiler built with AddressSanitizer
|
static bool devAsan(); // 'verilator_bin' built with AddressSanitizer
|
||||||
static bool devGcov(); // Compiler built with code coverage for gcov
|
static bool devGcov(); // 'verilator_bin' built with code coverage for gcov
|
||||||
|
static bool tsanSupport(); // Configured C++ compiler supports ThreadSanitizer
|
||||||
|
|
||||||
// METHODS (file utilities using these options)
|
// METHODS (file utilities using these options)
|
||||||
string fileExists(const string& filename);
|
string fileExists(const string& filename);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ PACKAGE_VERSION_STRING_CHAR
|
||||||
// Define if coroutines are supported on this platform
|
// Define if coroutines are supported on this platform
|
||||||
#undef HAVE_COROUTINES
|
#undef HAVE_COROUTINES
|
||||||
|
|
||||||
|
// Define if ThreadSanitizer is supported on this platform
|
||||||
|
#undef HAVE_TSAN
|
||||||
|
|
||||||
// Define if compiled with tcmalloc
|
// Define if compiled with tcmalloc
|
||||||
#undef HAVE_TCMALLOC
|
#undef HAVE_TCMALLOC
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,7 @@ class Capabilities:
|
||||||
_cached_have_lldb = None
|
_cached_have_lldb = None
|
||||||
_cached_have_sc = None
|
_cached_have_sc = None
|
||||||
_cached_have_solver = None
|
_cached_have_solver = None
|
||||||
|
_cached_have_tsan = None
|
||||||
_cached_make_version = None
|
_cached_make_version = None
|
||||||
|
|
||||||
@staticproperty
|
@staticproperty
|
||||||
|
|
@ -226,6 +227,12 @@ class Capabilities:
|
||||||
Capabilities._cached_have_solver = bool('usage' in out.casefold())
|
Capabilities._cached_have_solver = bool('usage' in out.casefold())
|
||||||
return Capabilities._cached_have_solver
|
return Capabilities._cached_have_solver
|
||||||
|
|
||||||
|
@staticproperty
|
||||||
|
def have_tsan() -> bool: # pylint: disable=no-method-argument
|
||||||
|
if Capabilities._cached_have_tsan is None:
|
||||||
|
Capabilities._cached_have_tsan = bool(Capabilities._verilator_get_supported('TSAN'))
|
||||||
|
return Capabilities._cached_have_tsan
|
||||||
|
|
||||||
@staticproperty
|
@staticproperty
|
||||||
@lru_cache(maxsize=1024)
|
@lru_cache(maxsize=1024)
|
||||||
def make_version() -> str: # pylint: disable=no-method-argument
|
def make_version() -> str: # pylint: disable=no-method-argument
|
||||||
|
|
@ -248,6 +255,7 @@ class Capabilities:
|
||||||
_ignore = Capabilities.have_lldb
|
_ignore = Capabilities.have_lldb
|
||||||
_ignore = Capabilities.have_sc
|
_ignore = Capabilities.have_sc
|
||||||
_ignore = Capabilities.have_solver
|
_ignore = Capabilities.have_solver
|
||||||
|
_ignore = Capabilities.have_tsan
|
||||||
|
|
||||||
# Internals
|
# Internals
|
||||||
|
|
||||||
|
|
@ -881,6 +889,7 @@ class VlTest:
|
||||||
self.verilator_make_gmake = True
|
self.verilator_make_gmake = True
|
||||||
self.verilator_make_cmake = False
|
self.verilator_make_cmake = False
|
||||||
self.verilated_debug = Args.verilated_debug
|
self.verilated_debug = Args.verilated_debug
|
||||||
|
self.tsan = Args.tsan
|
||||||
|
|
||||||
self._status_filename = self.obj_dir + "/V" + self.name + ".status"
|
self._status_filename = self.obj_dir + "/V" + self.name + ".status"
|
||||||
self.coverage_filename = self.obj_dir + "/coverage.dat"
|
self.coverage_filename = self.obj_dir + "/coverage.dat"
|
||||||
|
|
@ -949,6 +958,14 @@ class VlTest:
|
||||||
self._skips = message
|
self._skips = message
|
||||||
raise VtSkipException
|
raise VtSkipException
|
||||||
|
|
||||||
|
def enable_tsan(self) -> None:
|
||||||
|
"""Called from tests as: enable_tsan() to run this test with
|
||||||
|
ThreadSanitizer (as if --tsan was given), or skip the test if
|
||||||
|
ThreadSanitizer is not available."""
|
||||||
|
if not self.have_tsan:
|
||||||
|
self.skip("No ThreadSanitizer-capable C++ compiler available")
|
||||||
|
self.tsan = True
|
||||||
|
|
||||||
def priority(self, level: int) -> None:
|
def priority(self, level: int) -> None:
|
||||||
"""Called from tests as: priority(<constant_number>) to
|
"""Called from tests as: priority(<constant_number>) to
|
||||||
specify what priority order the test should run at.
|
specify what priority order the test should run at.
|
||||||
|
|
@ -1138,6 +1155,9 @@ class VlTest:
|
||||||
|
|
||||||
if re.search(r'-runtime-debug', checkflags):
|
if re.search(r'-runtime-debug', checkflags):
|
||||||
self._uses_asan = True
|
self._uses_asan = True
|
||||||
|
if self.tsan:
|
||||||
|
# --runtime-debug adds -fsanitize=address, incompatible with thread
|
||||||
|
self.skip("ThreadSanitizer not compatible with AddressSanitizer\n")
|
||||||
|
|
||||||
verilator_flags = [*param.get('verilator_flags', "")]
|
verilator_flags = [*param.get('verilator_flags', "")]
|
||||||
if Args.gdb:
|
if Args.gdb:
|
||||||
|
|
@ -1150,6 +1170,8 @@ class VlTest:
|
||||||
verilator_flags += ["--trace-vcd"]
|
verilator_flags += ["--trace-vcd"]
|
||||||
if Args.gdbsim or Args.rrsim:
|
if Args.gdbsim or Args.rrsim:
|
||||||
verilator_flags += ["-CFLAGS -ggdb -LDFLAGS -ggdb"]
|
verilator_flags += ["-CFLAGS -ggdb -LDFLAGS -ggdb"]
|
||||||
|
if self.tsan:
|
||||||
|
verilator_flags += ["-CFLAGS -fsanitize=thread -CFLAGS -g -LDFLAGS -fsanitize=thread"]
|
||||||
verilator_flags += ["--x-assign unique"] # More likely to be buggy
|
verilator_flags += ["--x-assign unique"] # More likely to be buggy
|
||||||
|
|
||||||
if param['vltmt']:
|
if param['vltmt']:
|
||||||
|
|
@ -1370,6 +1392,10 @@ class VlTest:
|
||||||
"Test requires CMake; ignore error since not available or version too old\n")
|
"Test requires CMake; ignore error since not available or version too old\n")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if param['verilator_make_cmake'] and self.tsan:
|
||||||
|
self.skip("ThreadSanitizer flags not supported with CMake build\n")
|
||||||
|
return
|
||||||
|
|
||||||
if not param['fails'] and param['make_main']:
|
if not param['fails'] and param['make_main']:
|
||||||
self._make_main(param['timing_loop'], param['main_top_name'])
|
self._make_main(param['timing_loop'], param['main_top_name'])
|
||||||
|
|
||||||
|
|
@ -1520,6 +1546,10 @@ class VlTest:
|
||||||
run_env = param['run_env']
|
run_env = param['run_env']
|
||||||
if run_env:
|
if run_env:
|
||||||
run_env = run_env + ' '
|
run_env = run_env + ' '
|
||||||
|
if self.tsan:
|
||||||
|
# Use default suppressions; environment TSAN_OPTIONS may override
|
||||||
|
run_env = ('TSAN_OPTIONS="suppressions=' + os.environ['TEST_REGRESS'] +
|
||||||
|
'/tsan.supp $TSAN_OPTIONS" ' + run_env)
|
||||||
|
|
||||||
if param['atsim']:
|
if param['atsim']:
|
||||||
cmd = [
|
cmd = [
|
||||||
|
|
@ -1814,6 +1844,10 @@ class VlTest:
|
||||||
self._have_solver_called = True
|
self._have_solver_called = True
|
||||||
return Capabilities.have_solver
|
return Capabilities.have_solver
|
||||||
|
|
||||||
|
@property
|
||||||
|
def have_tsan(self) -> bool:
|
||||||
|
return Capabilities.have_tsan
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def make_version(self) -> str:
|
def make_version(self) -> str:
|
||||||
return Capabilities.make_version
|
return Capabilities.make_version
|
||||||
|
|
@ -2998,6 +3032,9 @@ if __name__ == '__main__':
|
||||||
parser.add_argument('--stop', action='store_true', help='stop on the first error')
|
parser.add_argument('--stop', action='store_true', help='stop on the first error')
|
||||||
parser.add_argument("--top-filename", help="override the default Verilog file name")
|
parser.add_argument("--top-filename", help="override the default Verilog file name")
|
||||||
parser.add_argument('--trace', action='store_true', help='enable simulator waveform tracing')
|
parser.add_argument('--trace', action='store_true', help='enable simulator waveform tracing')
|
||||||
|
parser.add_argument('--tsan',
|
||||||
|
action='store_true',
|
||||||
|
help='run Verilated executable with ThreadSanitizer')
|
||||||
parser.add_argument('--verbose',
|
parser.add_argument('--verbose',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='compile and run test in verbose mode')
|
help='compile and run test in verbose mode')
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,12 @@ if test.have_sc:
|
||||||
expect_filename="t/t_flag_supported_1.out",
|
expect_filename="t/t_flag_supported_1.out",
|
||||||
verilator_run=True)
|
verilator_run=True)
|
||||||
|
|
||||||
|
if test.have_tsan:
|
||||||
|
test.run(cmd=[os.environ["VERILATOR_ROOT"] + "/bin/verilator --get-supported TSAN"],
|
||||||
|
logfile=test.obj_dir + "/vlt_tsan.log",
|
||||||
|
expect_filename="t/t_flag_supported_1.out",
|
||||||
|
verilator_run=True)
|
||||||
|
|
||||||
test.run(cmd=[os.environ["VERILATOR_ROOT"] + "/bin/verilator --get-supported DOES_NOT_EXIST"],
|
test.run(cmd=[os.environ["VERILATOR_ROOT"] + "/bin/verilator --get-supported DOES_NOT_EXIST"],
|
||||||
logfile=test.obj_dir + "/vlt_does_not_exist.log",
|
logfile=test.obj_dir + "/vlt_does_not_exist.log",
|
||||||
expect_filename="t/t_flag_supported_empty.out",
|
expect_filename="t/t_flag_supported_empty.out",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it
|
||||||
|
# under the terms of either the GNU Lesser General Public License Version 3
|
||||||
|
# or the Perl Artistic License Version 2.0.
|
||||||
|
# SPDX-FileCopyrightText: 2026 Wilson Snyder
|
||||||
|
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
|
||||||
|
|
||||||
|
import vltest_bootstrap
|
||||||
|
|
||||||
|
test.scenarios('vltmt')
|
||||||
|
|
||||||
|
# Very hard to reliably trigger run-time race on small design,
|
||||||
|
# use ThreadSanitizer to test
|
||||||
|
test.enable_tsan()
|
||||||
|
|
||||||
|
test.compile(verilator_flags2=['--binary', '-fno-dfg', '--no-threads-coarsen'], threads=2)
|
||||||
|
|
||||||
|
test.execute(fails='any') # Now failing, fix pending
|
||||||
|
|
||||||
|
test.passes()
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// DESCRIPTION: Verilator: Verilog Test module
|
||||||
|
//
|
||||||
|
// This file ONLY is placed under the Creative Commons Public Domain.
|
||||||
|
// SPDX-FileCopyrightText: 2026 Wilson Snyder
|
||||||
|
// SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
// Multi-threaded scheduling hazard on combinational loop variables.
|
||||||
|
|
||||||
|
module t;
|
||||||
|
logic clk = 0;
|
||||||
|
always #5 clk = ~clk;
|
||||||
|
|
||||||
|
logic [31:0] cyc = 0;
|
||||||
|
|
||||||
|
// verilator lint_off UNOPTFLAT
|
||||||
|
logic xxx, yyy;
|
||||||
|
// verilator lint_on UNOPTFLAT
|
||||||
|
|
||||||
|
// verilator lint_off ALWCOMBORDER
|
||||||
|
always_comb xxx = cyc[0] ? ~yyy : xxx; // xxx will become hybrid sensitivity
|
||||||
|
always_comb yyy = cyc[0] ? ~xxx : yyy; // yyy will become hybrid sensitivity
|
||||||
|
// verilator lint_on ALWCOMBORDER
|
||||||
|
|
||||||
|
always @(posedge clk) begin
|
||||||
|
cyc <= cyc + 1;
|
||||||
|
if (cyc == 10) begin
|
||||||
|
$display("xxx^yyy=%x", xxx ^ yyy);
|
||||||
|
$write("*-* All Finished *-*\n");
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
endmodule
|
||||||
|
|
@ -11,6 +11,9 @@ import vltest_bootstrap
|
||||||
|
|
||||||
test.scenarios('vlt_all')
|
test.scenarios('vlt_all')
|
||||||
|
|
||||||
|
if test.tsan:
|
||||||
|
test.skip("ThreadSanitizer does not support fork from a multithreaded process")
|
||||||
|
|
||||||
test.compile(make_top_shell=False,
|
test.compile(make_top_shell=False,
|
||||||
make_main=False,
|
make_main=False,
|
||||||
verilator_flags2=["--exe", test.pli_filename, "-cc"],
|
verilator_flags2=["--exe", test.pli_filename, "-cc"],
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ def run(test, *, verilator_flags2=()):
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
test.skip("Skipping on Windows: test depends on Unix-style shared-library loading")
|
test.skip("Skipping on Windows: test depends on Unix-style shared-library loading")
|
||||||
|
|
||||||
|
if test.tsan:
|
||||||
|
test.skip("ThreadSanitizer instrumented library cannot be loaded by python")
|
||||||
|
|
||||||
# All test use the same SV file
|
# All test use the same SV file
|
||||||
test.top_filename = "t/t_trace_lib_as_top.v"
|
test.top_filename = "t/t_trace_lib_as_top.v"
|
||||||
test.pli_filename = os.path.abspath("t/t_trace_lib_as_top.cpp")
|
test.pli_filename = os.path.abspath("t/t_trace_lib_as_top.cpp")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
# DESCRIPTION: Verilator: ThreadSanitizer default suppressions for test_regress
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it
|
||||||
|
# under the terms of either the GNU Lesser General Public License Version 3
|
||||||
|
# or the Perl Artistic License Version 2.0.
|
||||||
|
# SPDX-FileCopyrightText: 2026 Wilson Snyder
|
||||||
|
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
|
||||||
|
#
|
||||||
|
# This file is used via TSAN_OPTIONS by driver.py when running a test with
|
||||||
|
# ThreadSanitizer (--tsan option, or test.enable_tsan()). Suppress known
|
||||||
|
# races in the runtime library. TODO: most of these should likely be fixed.
|
||||||
|
|
||||||
|
# Racy update of the cached last-used context pointer
|
||||||
|
race:*Verilated::lastContextp*
|
||||||
|
|
||||||
|
# Model teardown may destroy mtask state while a worker thread is still
|
||||||
|
# returning from signaling completion of the final mtask
|
||||||
|
race:*VlMTaskVertex::signalUpstreamDone*
|
||||||
|
|
||||||
|
# The parallel trace buffer handoff between worker threads and the commit
|
||||||
|
# on the dumping thread is not visible to ThreadSanitizer; this includes
|
||||||
|
# the trace worker unlocking a mutex locked by the dumping thread
|
||||||
|
race:*VerilatedTrace*
|
||||||
|
race:*VerilatedVcd*
|
||||||
|
mutex:*VerilatedTrace*
|
||||||
|
|
||||||
|
# vl_stop (e.g. on assertion failure) tears the model down via exit
|
||||||
|
# handlers while worker threads may still be executing the current eval
|
||||||
|
race:*vl_stop*
|
||||||
|
|
||||||
|
# The --debug-runtime-timeout alarm handler prints via the runtime, which
|
||||||
|
# allocates; not async-signal-safe. Note 'signal:' matches the handler name.
|
||||||
|
signal:*alarmHandler*
|
||||||
Loading…
Reference in New Issue