ADR-013: Suppression System Design¶
Date: 2026-03-18 Status: Accepted Decision maker: Nikolay Petrov
Context¶
Real-world ABI analysis produces findings that are technically correct but operationally irrelevant: internal symbols exposed by visibility leaks, deprecated APIs intentionally removed, known-safe type changes. Users need a mechanism to suppress specific findings without disabling entire detectors.
Requirements¶
- Suppress by symbol name (exact or pattern)
- Suppress by type name (for type-level changes only)
- Suppress by change kind
- Support temporary suppressions (expiry dates)
- Maintain an audit trail of suppressed changes
- Support ABICC skip/whitelist format for migration (Goal 1)
- Resist regex-based denial of service (ReDoS)
Options considered¶
| Option | Description | Trade-off |
|---|---|---|
| A: JSON suppression files | Structured, schema-validated | Verbose for humans |
| B: YAML suppression files | Human-readable, concise | YAML parsing pitfalls (Norway problem, anchors) |
| C: Inline comments in source | Co-located with code | Requires source access; doesn't work for binary-only analysis |
Decision¶
YAML format¶
suppressions:
- symbol: _ZN3foo3barEv
reason: "Internal API — not part of public contract"
- symbol_pattern: "_ZN3foo.*Internal.*"
change_kind: func_removed
reason: "Entire internal namespace being cleaned up"
expires: "2026-06-01"
- type_pattern: ".*_internal_t"
reason: "Internal types — layout changes are expected"
- source_location: "include/internal/*.h"
reason: "All changes in internal headers are suppressed"
label: "internal"
YAML was chosen over JSON for human readability. PyYAML is already a
dependency (used for policy files). The defusedxml security approach is
applied to YAML: yaml.safe_load() only — no arbitrary Python object
construction.
Suppression rule model¶
@dataclass
class SuppressionRule:
symbol: str | None # Exact symbol match
symbol_pattern: str | None # Regex (fullmatch semantics)
type_pattern: str | None # Regex for type-level changes only
change_kind: str | None # Filter by ChangeKind value
reason: str | None # Documentation
label: str | None # Grouping tag (e.g., "workaround")
source_location: str | None # fnmatch glob against source path
expires: date | None # ISO 8601 date — inactive after expiry
Matching semantics¶
Selector exclusivity: Exactly one of symbol, symbol_pattern,
type_pattern, or source_location must be specified per rule. This is
validated at load time — malformed rules produce immediate errors, not silent
no-ops.
Fullmatch semantics: Pattern matching uses re.fullmatch() — the pattern
must match the entire symbol name, not a substring. This prevents
over-suppression from partial matches.
Type pattern scoping: type_pattern only matches changes whose
ChangeKind is in the _TYPE_CHANGE_KINDS set. This prevents a type
whitelist from accidentally suppressing symbol-level changes on identically
named symbols.
Source location matching: source_location uses fnmatch glob syntax
(not regex). The match is against the file path portion of
change.source_location (strips :line[:col] suffix).
Conjunctive matching: When change_kind is specified alongside a selector,
both must match. The change kind narrows the selector — it does not act as an
independent filter.
Regex safety¶
Python's re module is used with re.compile() for pattern compilation.
Patterns are compiled eagerly at rule load time — malformed patterns produce
immediate errors. Matching uses fullmatch() which applies the pattern to the
complete string.
While the documentation references RE2-style safety (guaranteed O(N)), the
implementation uses Python's standard re library. Complex patterns with
pathological backtracking are possible in theory but unlikely in practice for
symbol name patterns. For production deployments processing untrusted
suppression files, consider validating pattern complexity at load time.
Expiry mechanism¶
def is_expired(self, today: date | None = None) -> bool:
if self.expires is None:
return False
return (today or date.today()) > self.expires
- Expired rules never match — they are silently skipped during filtering
expired_rules()method returns a list of expired rules for warning generation- YAML loader handles both ISO 8601 strings (
"2026-06-01") and native YAML date values datetimeobjects are converted todateto avoidTypeErrorin comparison
Pipeline ordering¶
Suppression is applied at a specific point in the compare() pipeline:
[30 detectors]
→ _deduplicate_ast_dwarf(changes) # AST↔DWARF dedup
→ suppress.filter(changes) # ← Suppression applied here
→ _filter_redundant(unsuppressed) # Redundancy filtering (ADR-004)
→ _enrich_affected_symbols(kept) # Symbol enrichment
→ compute_verdict(kept + redundant) # Verdict on unsuppressed only
Critical design choice: Suppression runs before redundancy filtering. This ensures that a suppressed change never contributes to the verdict — whether it would have been classified as a root change or a redundant derived change. See ADR-004 for the complete pipeline design including redundancy filtering and leaf-change mode.
Audit trail¶
Suppressed changes are preserved in DiffResult.suppressed_changes:
@dataclass
class DiffResult:
changes: list[Change]
suppressed_changes: list[Change] # filtered by suppression
suppressed_count: int
Reports include a suppression summary section showing how many changes were suppressed and which rules matched. This ensures suppressions are visible and auditable.
ABICC format support¶
For ABICC migration (ADR-012), the compat layer converts ABICC skip/whitelist
files to native SuppressionRule objects:
-skip-symbolsplain-text file →SuppressionRule(symbol=...)orSuppressionRule(symbol_pattern=...)depending on regex character detection-skip-typesplain-text file →SuppressionRule(symbol=..., change_kind=...)scoped to type-level changes- Unmangled C function names get an automatic Itanium mangling pattern
fallback:
_Z\d+{name}.*
Validation¶
- Unknown keys in suppression entries are rejected (not silently ignored)
change_kindvalues are validated against_VALID_CHANGE_KINDSfrozenset- Missing required selector (none of symbol/pattern/type_pattern/source_location) produces an error at load time
Example error for malformed rules:
# Invalid: no selector specified
- change_kind: func_removed
reason: "example"
→ Error: suppression rule 1: must specify exactly one of symbol,
symbol_pattern, type_pattern, or source_location
# Invalid: multiple selectors
- symbol: _ZN3foo
symbol_pattern: "_ZN3foo.*"
→ Error: suppression rule 2: only one of symbol/symbol_pattern/
type_pattern/source_location allowed
Consequences¶
Positive¶
- Fine-grained control over which findings appear in reports
- Expiry dates prevent stale suppressions from hiding real regressions
- Audit trail ensures suppressions are visible and reviewable
- ABICC skip file compatibility enables smooth migration
- Pipeline ordering guarantees suppressions affect verdicts correctly
Negative¶
- YAML has well-known pitfalls (Norway problem, implicit type coercion) —
mitigated by
safe_load()and explicit validation - Two suppression formats (YAML + ABICC text) adds maintenance burden
fullmatchsemantics may surprise users expecting substring matching- Regex patterns in YAML require quoting to avoid YAML syntax conflicts
References¶
abicheck/suppression.py—SuppressionRule,SuppressionList, matching logicabicheck/compat/cli.py— ABICC skip list conversionabicheck/checker.py— Pipeline ordering (suppression before redundancy)- ADR-004 — Report filtering and deduplication (redundancy filtering stage)