mirror of
https://gitlab.freedesktop.org/xorg/xserver.git
synced 2026-06-07 11:08:21 +02:00
This test suite is primarily aimed at reproducing the various CVE issues we've had over the years that require custom crafted protocol requests. It may also be useful for other testing. Wrapped in python because pytest is a powerful test suite runner and writing custom buffers is easy. The architecture is so that we fork off an X server (one or more of Xvfb, Xwayland, Xorg) and then run our test clients against that to check whether we get the right reply, or crash the server, or whether valgrind complains about something (valgrind is started automatically for tests that are marked as such). Tests can be run manually via pytest or via meson test. Assisted-by: Claude:claude-claude-opus-4-6 Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2187>
117 lines
3.9 KiB
Python
117 lines
3.9 KiB
Python
# SPDX-License-Identifier: MIT
|
|
#
|
|
# AddressSanitizer (ASAN) output parser for extracting memory errors.
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
|
|
@dataclass
|
|
class AsanError:
|
|
"""Represents a single ASAN error extracted from log/stderr output."""
|
|
|
|
kind: str # e.g. "heap-buffer-overflow", "heap-use-after-free"
|
|
description: str # The full ERROR line
|
|
stack_frames: list[tuple[str, str | None, str | None]] # (func, file, line)
|
|
|
|
def __str__(self):
|
|
lines = [f"{self.kind}: {self.description}"]
|
|
for func, srcfile, line in self.stack_frames[:8]:
|
|
loc = f" ({srcfile}:{line})" if srcfile else ""
|
|
lines.append(f" at {func}{loc}")
|
|
return "\n".join(lines)
|
|
|
|
@classmethod
|
|
def from_log(cls, log_path: Path) -> list[AsanError]:
|
|
"""Parse ASAN output from a log file.
|
|
|
|
ASAN may append a PID suffix to the log_path, so we glob for
|
|
matching files (e.g. log_path.1234).
|
|
"""
|
|
errors: list[AsanError] = []
|
|
|
|
# ASAN appends .<pid> to the log_path
|
|
candidates = list(log_path.parent.glob(f"{log_path.name}.*"))
|
|
if log_path.is_file():
|
|
candidates.append(log_path)
|
|
|
|
for path in candidates:
|
|
try:
|
|
text = path.read_text(errors="replace")
|
|
except OSError:
|
|
continue
|
|
errors.extend(cls.from_text(text))
|
|
|
|
return errors
|
|
|
|
@classmethod
|
|
def from_text(cls, text: str) -> list[AsanError]:
|
|
"""Parse ASAN errors from text output (stderr or log file).
|
|
|
|
Recognises the standard ASAN report format::
|
|
|
|
==PID==ERROR: AddressSanitizer: heap-buffer-overflow on ...
|
|
READ of size 4 at 0x... thread T0
|
|
#0 0xaddr in func file.c:123
|
|
#1 0xaddr in func2 file2.c:456
|
|
|
|
SUMMARY: AddressSanitizer: heap-buffer-overflow ...
|
|
"""
|
|
errors: list[AsanError] = []
|
|
|
|
# Pattern for the ERROR line
|
|
error_re = re.compile(r"==\d+==ERROR: AddressSanitizer: ([\w-]+)(.*)")
|
|
# Pattern for stack frames:
|
|
# #0 0x... in function_name file.c:123:45
|
|
# #0 0x... in function_name (module+0xoffset)
|
|
# #0 0x... (module+0xoffset)
|
|
frame_re = re.compile(
|
|
r"\s+#\d+\s+\S+\s+in\s+(\S+)\s+(\S+?)(?::(\d+))?(?::\d+)?\s*$"
|
|
)
|
|
# Frame without source info (just address in module)
|
|
frame_nosrc_re = re.compile(r"\s+#\d+\s+\S+\s+in\s+(\S+)")
|
|
|
|
lines = text.splitlines()
|
|
i = 0
|
|
while i < len(lines):
|
|
m = error_re.search(lines[i])
|
|
if not m:
|
|
i += 1
|
|
continue
|
|
|
|
kind = m.group(1)
|
|
description = m.group(2).strip()
|
|
|
|
# Collect stack frames following the ERROR line
|
|
frames: list[tuple[str, str | None, str | None]] = []
|
|
i += 1
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
# Stop at blank lines, SUMMARY lines, or new ERROR lines
|
|
if not line.strip() or line.strip().startswith("SUMMARY:"):
|
|
break
|
|
if error_re.search(line):
|
|
break
|
|
|
|
fm = frame_re.match(line)
|
|
if fm:
|
|
func = fm.group(1)
|
|
srcfile = fm.group(2)
|
|
lineno = fm.group(3)
|
|
# Filter out non-file entries like (module+0xoffset)
|
|
if srcfile and srcfile.startswith("("):
|
|
srcfile = None
|
|
lineno = None
|
|
frames.append((func, srcfile, lineno))
|
|
else:
|
|
fm2 = frame_nosrc_re.match(line)
|
|
if fm2:
|
|
frames.append((fm2.group(1), None, None))
|
|
i += 1
|
|
|
|
errors.append(cls(kind=kind, description=description, stack_frames=frames))
|
|
|
|
return errors
|