import mozunit from moztest.tsan import TSANErrorParser class FakeLogger: def __init__(self): self.errors = [] def tsan_error(self, **data): self.errors.append(data) LOCK_ORDER = """\ WARNING: ThreadSanitizer: lock-order-inversion (potential deadlock) (pid=1666) Cycle in lock order graph: M0 (0x1) => M1 (0x2) => M0 Mutex M1 acquired here while holding mutex M0 in main thread: #0 pthread_mutex_lock /build/tsan_interceptors_posix.cpp:1371:3 (firefox+0xc7827) (BuildId: abc123) #1 mutexLock /build/mozglue/misc/Mutex_posix.cpp:91:3 (firefox+0x1bd5b7) Mutex M0 acquired here while holding mutex M1 in thread T4: #0 SharedStub xptcstubs_x86_64_linux.cpp (libxul.so+0x4929d32) (BuildId: def456) #1 mozilla::Foo::Bar() (libxul.so+0x10) SUMMARY: ThreadSanitizer: lock-order-inversion (potential deadlock) /build/mozglue/misc/Mutex_posix.cpp:91:3 in mutexLock """ DATA_RACE = """\ WARNING: ThreadSanitizer: data race (pid=7061) Write of size 8 at 0x55 by main thread: #0 js::Activation::registerProfiling() /build/js/src/vm/Activation.cpp:16:29 (libxul.so+0xb1) (BuildId: abc123) SUMMARY: ThreadSanitizer: data race /build/js/src/vm/Activation.cpp:16:29 in js::Activation::registerProfiling() """ SEGV = """\ ThreadSanitizer:DEADLYSIGNAL ==2708==ERROR: ThreadSanitizer: SEGV on unknown address 0x000000000000 (pc 0x7f8c9a72a810 bp 0x000000000e2e sp 0x7f8b643fa610 T8177) ==2708==The signal is caused by a WRITE memory access. ==2708==Hint: address points to the zero page. #0 MOZ_CrashSequence /builds/worker/workspace/obj-build/dist/include/mozilla/Assertions.h:261:3 (libxul.so+0xb72a810) (BuildId: 2242523b37b4cfbbb67488eeaec361db644a3337) #1 MOZ_Crash /builds/worker/workspace/obj-build/dist/include/mozilla/Assertions.h:402:3 (libxul.so+0xb72a810) #2 mozilla::(anonymous namespace)::RunWatchdog(void*) /builds/worker/checkouts/gecko/toolkit/components/terminator/nsTerminator.cpp:238:5 (libxul.so+0xb72a810) ==2708==Register values: rax = 0x00000000000000ee rbx = 0x000072180001bde0 rcx = 0x00007f8baec803a0 ThreadSanitizer can not provide additional info. SUMMARY: ThreadSanitizer: SEGV /builds/worker/workspace/obj-build/dist/include/mozilla/Assertions.h:261:3 in MOZ_CrashSequence ==2708==ABORTING """ def feed(parser, text, pid, scope=None): for line in text.splitlines(): parser.log(line, pid=pid, scope=scope) def test_lock_order_report(): logger = FakeLogger() parser = TSANErrorParser(logger) feed(parser, LOCK_ORDER, pid="A", scope="browser/foo") assert len(logger.errors) == 1 report = logger.errors[0] assert report["kind"] == "lock-order-inversion (potential deadlock)" assert report["pid"] == 1666 assert report["scope"] == "browser/foo" assert report["signature"] == "Mutex_posix.cpp:91:3 in mutexLock" assert report["description"] == ( "Cycle in lock order graph: M0 (0x1) => M1 (0x2) => M0" ) stacks = report["stacks"] assert len(stacks) == 2 assert ( stacks[0]["label"] == "Mutex M1 acquired here while holding mutex M0 in main thread" ) assert ( stacks[1]["label"] == "Mutex M0 acquired here while holding mutex M1 in thread T4" ) # Fully symbolized frame with path and line:column. top = stacks[0]["stack"][0] assert top == { "function": "pthread_mutex_lock", "module": "firefox", "module_offset": "0xc7827", "file": "/build/tsan_interceptors_posix.cpp", "line": 1371, "column": 3, } # A bare-filename frame (no line) and a frame with no file at all. shared_stub, foo_bar = stacks[1]["stack"] assert shared_stub["function"] == "SharedStub" assert shared_stub["file"] == "xptcstubs_x86_64_linux.cpp" assert "line" not in shared_stub assert foo_bar["function"] == "mozilla::Foo::Bar()" assert foo_bar["module"] == "libxul.so" assert "file" not in foo_bar def test_data_race_report(): logger = FakeLogger() parser = TSANErrorParser(logger) feed(parser, DATA_RACE, pid="B") assert len(logger.errors) == 1 report = logger.errors[0] assert report["kind"] == "data race" assert report["pid"] == 7061 assert report["description"] is None assert report["signature"] == ( "Activation.cpp:16:29 in js::Activation::registerProfiling()" ) assert len(report["stacks"]) == 1 assert report["stacks"][0]["label"] == "Write of size 8 at 0x55 by main thread" def test_segv_report(): # A signal report uses a "==pid==ERROR:" header, has descriptive noise in # the kind, and lists frames with no preceding label line. logger = FakeLogger() parser = TSANErrorParser(logger) feed(parser, SEGV, pid="S") assert len(logger.errors) == 1 report = logger.errors[0] assert report["kind"] == "SEGV" assert report["pid"] == 2708 assert report["signature"] == "Assertions.h:261:3 in MOZ_CrashSequence" # The label-less frames are collected under a single implicit stack. assert len(report["stacks"]) == 1 assert report["stacks"][0]["label"] == "" funcs = [f["function"] for f in report["stacks"][0]["stack"]] assert funcs == [ "MOZ_CrashSequence", "MOZ_Crash", "mozilla::(anonymous namespace)::RunWatchdog(void*)", ] def test_interleaved_streams(): # Reports arriving on two different emitting streams, line-interleaved, # must be kept separate by pid. logger = FakeLogger() parser = TSANErrorParser(logger) a = LOCK_ORDER.splitlines() b = DATA_RACE.splitlines() for i in range(max(len(a), len(b))): if i < len(a): parser.log(a[i], pid="A") if i < len(b): parser.log(b[i], pid="B") assert len(logger.errors) == 2 kinds = {e["kind"] for e in logger.errors} assert kinds == {"lock-order-inversion (potential deadlock)", "data race"} for e in logger.errors: # Each report kept its own (non-empty) stacks. assert e["stacks"] assert all(s["stack"] for s in e["stacks"]) def test_truncated_report_flushed_on_flush(): logger = FakeLogger() parser = TSANErrorParser(logger) # Header + one stack, but no SUMMARY line (output cut off). truncated = "\n".join(LOCK_ORDER.splitlines()[:5]) feed(parser, truncated, pid="C") assert logger.errors == [] parser.flush() assert len(logger.errors) == 1 report = logger.errors[0] # No SUMMARY was seen, so the signature falls back to the kind. assert report["signature"] == report["kind"] assert len(report["stacks"]) == 1 def test_non_tsan_output_is_ignored(): logger = FakeLogger() parser = TSANErrorParser(logger) for line in ["just some output", " #0 not in a report (libxul.so+0x1)", ""]: parser.log(line, pid="A") parser.flush() assert logger.errors == [] if __name__ == "__main__": mozunit.main()