Policy Profiles¶
abicheck compare supports policy-based verdict classification.
Policy classification is one stage of the CI gating pipeline — it runs after contract relevance (under
--contract) has decided which findings are even in scope, and before suppression and severity/exit-code scoring. See CI Gating for the full order and how policy combines with contract relevance, suppressions, severity, and baselines.
- Built-in profiles:
--policy strict_abi|sdk_vendor|plugin_abi - Custom profile file:
--policy <yaml>
Usage¶
abicheck compare old.json new.json --policy strict_abi # default
abicheck compare old.json new.json --policy sdk_vendor
abicheck compare old.json new.json --policy plugin_abi
abicheck compare old.json new.json --policy policy.yaml
Available Profiles¶
strict_abi (default)¶
Full strictness — every detected ABI change is classified at its maximum severity.
| Verdict | Meaning |
|---|---|
BREAKING |
Binary ABI break — old callers will crash or misbehave |
API_BREAK |
Source-level break — recompile required, but binary may still work |
COMPATIBLE_WITH_RISK |
Binary-compatible, but deployment risk present — verify target environments |
COMPATIBLE |
Safe addition or informational |
NO_CHANGE |
No differences found |
The soname_bump_recommended advisory is emitted as COMPATIBLE (quality issue) when
binary-incompatible changes are detected but the SONAME is not bumped. The underlying
breaking changes themselves carry the BREAKING verdict. Use custom policy files to
escalate soname_bump_recommended to break if you want SONAME-bump enforcement to
fail CI:
Use for: shared libraries, system libraries, public SDKs with strict compatibility guarantees.
sdk_vendor¶
Permissive profile for SDK / vendor libraries. Source-level-only changes
(renames, access changes) are downgraded from API_BREAK to COMPATIBLE
since SDK consumers typically use stable binary interfaces, not source-level names.
Downgraded to COMPATIBLE under sdk_vendor:
| Change Kind | Description |
|---|---|
enum_member_renamed |
Enum member name changed (value unchanged) |
field_renamed |
Struct/class field name changed |
method_access_changed |
Method access level changed |
field_access_changed |
Field access level changed |
source_level_kind_changed |
struct ↔ class keyword (binary-identical) |
removed_const_overload |
const overload removed |
param_default_value_removed |
Default argument removed |
All BREAKING kinds remain BREAKING — this profile does not suppress binary breaks.
Use for: vendor SDKs, optional library extensions, plugin APIs where source compat is not required.
plugin_abi¶
Relaxed profile for plugins that are built from the same toolchain as the host
at the same time. Calling-convention signals are downgraded to COMPATIBLE since
they are controlled by the build system rather than the library ABI contract.
Downgraded to COMPATIBLE under plugin_abi:
| Change Kind | Description |
|---|---|
calling_convention_changed |
DWARF DW_AT_calling_convention drift |
value_abi_trait_changed |
DWARF triviality heuristic (pass-by-reg vs pointer) |
All BREAKING kinds that are not calling-convention-related remain BREAKING.
toolchain_flag_drift is already COMPATIBLE in the default policy (informational),
so it is not part of the plugin downgrade set.
Use for: dynamically-loaded plugins, JNI/Python extension modules, hot-reload scenarios where the plugin and host are always rebuilt together.
Built-in use-case profiles¶
Beyond the three base policies, abicheck ships a catalog of turnkey, ecosystem-specific
profiles as YAML files under abicheck/policies/. A bare name resolves to the shipped
file, so they need no path:
Each profile builds on strict_abi and adjusts only where the ecosystem's documented
compatibility rules differ from the strict default. They are derived from primary-source
guidance, not invented heuristics.
| Profile | Ecosystem / source | What it changes vs strict_abi |
|---|---|---|
security |
checksec-style hardening | Promotes RELRO/PIE/canary/FORTIFY/NX regressions to break |
qt_kde_cpp |
KDE C++ Binary Compatibility rules (Qt points here too) | Promotes func_noexcept_removed to break; documents the virtual/layout/enum rules strict already enforces |
glibc_symbol_versioned |
glibc symbol-versioning discipline | Pins version-node removals to break, accepts compat-version requirement additions, flags dropped DT_NEEDED as risk |
msvc_pe |
MSVC C++ binary compatibility + x64 ABI | Pins calling_convention_changed to break; dropped import DLL → risk (no RPATH fallback on Windows) |
mach_o_dylib |
Apple Dynamic Library Design Guidelines | Pins compat_version_changed to break (dyld load check); dropped install-name dependency → risk |
rust_c_ffi |
Rust Reference / Cargo SemVer (no stable Rust ABI) | Keeps the C-FFI surface (repr(C)/extern "C") strict but demotes C++-object-model kinds to risk — they can't occur on a real C-FFI boundary |
gnome_parallel_install |
GNOME/GTK parallel-install evolution | Enforces both directions of SONAME discipline: pins soname_bump_recommended to break (broke ABI without bumping), and surfaces soname_bump_unnecessary as risk (bumped the major for nothing, fragmenting parallel-installed consumers) |
Why most profiles are thin: the default
strict_abialready classifies the hard native cases (symbol removal, layout, vtables, mangling, calling convention) correctly, so a profile mostly adds a named entry point, primary-source documentation, and the few genuine per-ecosystem divergences. Managed-runtime ecosystems (Java class-file linkage, .NET assembly metadata) and source-only ecosystems (Go, non-FFI Rust) need dedicated format frontends rather than a policy file.
Custom Policy Files (--policy)¶
Custom policy files let you keep all detectors enabled and only override how specific change kinds are classified.
Minimal example:
base_policy: strict_abi # optional, default strict_abi
overrides:
enum_member_renamed: ignore # break|warn|ignore
field_renamed: ignore
calling_convention_changed: warn
Semantics:
- break → BREAKING (exit code 4)
- warn → API_BREAK (exit code 2)
- risk → COMPATIBLE_WITH_RISK (exit code 0; deployment risk visible in output)
- ignore → COMPATIBLE (exit code 0)
- kinds not listed in overrides use base_policy
If both --policy and --policy are provided, --policy wins.
Selector-scoped reclassification (reclassify)¶
overrides above changes a verdict for every symbol of a given ChangeKind,
project-wide. Suppressions can target one symbol/pattern/
namespace, but only by deleting the finding. reclassify is the third
option: the same selector grammar suppression rules use — symbol,
symbol_pattern, type_pattern, member_name, namespace,
entity_namespace, cause_namespace, source_location, binding
(conjunctive-only, same caveat as suppress's binding: — see
Suppressions), expires — plus a required to, applied
instead of deleting the finding:
reclassify:
- kind: func_visibility_changed
symbol_pattern: "_ZN6oneapi3dal.*"
to: risk # break|warn|risk|ignore, same vocabulary as overrides
reason: "COMDAT-inline demotions; consumers already embed their own copy"
Use this when a whole symbol family shares a known, accepted cause (e.g.
dozens of COMDAT-inline visibility demotions in a template-heavy library)
that you don't want to downgrade project-wide (too broad — overrides)
or hide entirely (suppress — loses the evidence a reviewer may still
want to see).
kindis this block's spelling of the selector'schange_kind.- Rules are evaluated in file order; the first matching rule wins, since (unlike suppression, where every match has the same effect) two rules can disagree about which verdict to apply to the same finding.
- A
reclassifymatch takes priority over a same-kindoverridesentry (more specific wins), but a pipeline-computedeffective_verdict(pattern modulation) and the frozen-namespace verdict floor still take precedence over both — see ci-gating.md for the full classification order. - At least one selector is required, same as a suppression rule.
- Active
reclassify:rules are listed in the standard JSON report'spolicy_reclassifykey, alongsidepolicy_overrides, so a reclassified finding's report always carries a trace of the policy that steered it (report_schema_version2.30+). A finding whose effective verdict was actually decided by a selector-scoped rule (not a same-kindoverrides:entry) additionally carries its ownchange.reclassified_byfield, naming the deciding rule'slabel/reason/to(report_schema_version2.31+) — consumers like the PR-comment renderer use this to disclose the reclassification per-finding, not just as part of the active rule list.
Evidence-aware controls (evidence_policy)¶
When a compare also carries build/source evidence (build-info / source packs), an optional evidence_policy block tunes how each category of
evidence finding is classified — independent of the per-ChangeKind overrides
above:
evidence_policy:
source_only_findings: warn # ignore | warn | fail-api | fail-release
build_context_drift: warn # ignore | warn | fail-on-abi-relevant
graph_risk_findings: warn # ignore | warn | fail
require_evidence: # fail if a required layer is not comparable
build_context: false
source_abi: false
graph_summary: false
source_only_findings— L4 source-replay / API-only findings (macros, default args, inline/template/constexpr bodies).ignore→COMPATIBLE,warn→COMPATIBLE_WITH_RISK,fail-api/fail-release→API_BREAK(exit 2).build_context_drift— L3 build-flag / toolchain drift.fail-on-abi-relevantescalates only genuinely ABI-relevant drift (std/visibility/packing flags, export policy, toolchain) toAPI_BREAK; other drift stays a risk.graph_risk_findings— L5 reachability/impact risks.fail→API_BREAK.require_evidence— when a listed layer istruebut absent from either the baseline or target side of the compare, anevidence_required_missingfinding (API_BREAK) fails the run so a silently-degraded scan can't pass.
Each knob is unset by default: leaving it out keeps the finding's normal
category, so existing runs are unchanged. By the authority rule these knobs never turn
a source/build-only finding into a hard (artifact-proven) BREAKING verdict —
the strongest they reach is API_BREAK.
Your project's internal-namespace convention (internal_namespaces)¶
Several detectors — the internal-leak walk, reachability-aware suppression
(see Suppressions § Reachability-aware suppression),
and the L5 call-graph reachability walk (see
Unified Impact Assessment)
— need to recognize which namespaces are your project's private-implementation
convention, so a change inside one is a candidate for demotion/suppression
instead of a hard break. The default token set is detail/impl/internal/
__detail/_impl. If your project uses a different convention, declare it
once instead of repeating it in every suppression rule:
Each entry must match a ::-joined namespace segment exactly — not a
glob or regex — the same way the built-in detail/impl/internal/
__detail/_impl tokens do (priv matches acme::priv::Widget's priv
segment, but not acme::privhelpers::Widget). An empty (or omitted) list
keeps every detector's own built-in default — existing policy files are
unaffected.
Which namespaces are "not yet promised stable" (experimental_namespaces)¶
A separate, unrelated convention: internal_namespaces marks
implementation detail, while these mark declarations that are public but
not yet covered by a stability promise. Removing one adds an
EXPERIMENTAL_* finding alongside the ordinary break — it is an overlay,
not a substitute, so the plain func_removed is still reported and still
drives the verdict. The default set is experimental/preview:
experimental_namespaces:
- experimental
- preview
- v0 # only if your project really means v0 that way
Matching works exactly like internal_namespaces above — whole ::-joined
segments, no globbing — and an omitted list keeps the default.
v0 is not in the default set. A version segment says which version of
the API a declaration belongs to; it says nothing about what is promised
about it. Inline-versioned public APIs (namespace v0 { … }, re-exported via
an inline namespace so callers write the unversioned spelling) are a common
way to spell a library's current public API, and treating that segment as
experimental described a fully supported API as never having been promised.
If your project does use v0 to mean experimental, list it as above — the
behaviour is then identical to the old default.
Because the EXPERIMENTAL_* finding is an overlay, dropping v0 from the
default cannot hide anything: a removal under v0 still reports
func_removed and still yields a BREAKING verdict and the same exit code.
What the default no longer does is add an annotation asserting the removal was
expected.
Exit Codes¶
For abicheck compare, exit codes are the same for all policies — only the verdict changes:
| Exit Code | Verdict |
|---|---|
0 |
NO_CHANGE or COMPATIBLE |
0 |
COMPATIBLE_WITH_RISK (deployment risk, inspect output) |
2 |
API_BREAK (source-level break) |
4 |
BREAKING (binary ABI break) |
This is the legacy (verdict-based) scheme. Any active severity setting (a
--severity-*flag or a severity value in.abicheck.yml) switchescompareto severity-based exit codes, where1means an error-level finding — see CI Gating → the two exit-code schemes and the canonical exit code reference.
Reusable packs (--pack)¶
A --policy is one project's own overrides. When the same overrides
should be shared across projects, put them in a pack — a small versioned
YAML document (id/version/kind/assignments) selected with
compare --pack or scan --against ... --pack (repeatable):
A pack really configures the run: it changes the verdict and the exit code
exactly as the equivalent --policy overrides would. It just never wins
against one — an explicitly stated value (a --policy override, a
--severity-* flag, or .abicheck.yml)
always outranks a pack, and two selected packs disagreeing about the same
field are a usage error rather than a silent last-one-wins.
kind: contract and kind: gate packs carry the other two namespaces
(internal namespaces and contract.unresolved; and the severity levels —
there is no exit-code-scheme field for a gate pack to assign at all: the
scheme is fully automatic, purely derived from whether a severity setting is
in effect, see CI Gating → the two exit-code
schemes). contract.unresolved
needs --contract to have any effect — it configures the contract-coverage
exit, which is only computed when a domain is selected to measure coverage
of — so assigning it without that flag is a usage error rather than a
silently inert setting.
Where each form is accepted follows from what a command has to configure:
a kind: gate pack applies to scan --against the same way
--severity-preset given directly already does — scan's
exit code has honoured the resolved severity config since
the fix that closed the "scan never consults severity" gap, and a gate pack
is one more source for that same gate.
A kind: policy/kind: contract/kind: gate pack's policy.overrides/
surface.internal_namespaces/contract.unresolved/gate.*, similarly, all
apply uniformly to every library on a directory/package (release) compare
— the gate half folds into the release fan-out's own resolved GateOptions object, and contract.unresolved still needs
--contract on that same release comparison, exactly as above (see the
"7B's release-fan-out investigation landed" section of
the one-semantic-pipeline plan
for the history of that rejection's own removal).
scan --pack also requires --against, since a pack's only application
there is the baseline comparison. Each rejection above is a usage error
rather than a silently ignored flag.
For the full field vocabulary, the precedence rules, and what a resolution receipt records, see Compatibility evaluation configuration.
Extending Policies¶
Built-in profiles are defined in abicheck/checker_policy.py:
- SDK_VENDOR_COMPAT_KINDS — kinds downgraded to COMPATIBLE under sdk_vendor
- PLUGIN_ABI_DOWNGRADED_KINDS — kinds downgraded to COMPATIBLE under plugin_abi
Custom file parsing/overrides live in abicheck/policy_file.py (PolicyFile).