Skip to content

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:

overrides:
  soname_bump_recommended: break

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:

abicheck compare libfoo.so.1 libfoo.so.2 --policy qt_kde_cpp

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_abi already 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).

  • kind is this block's spelling of the selector's change_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 reclassify match takes priority over a same-kind overrides entry (more specific wins), but a pipeline-computed effective_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's policy_reclassify key, alongside policy_overrides, so a reclassified finding's report always carries a trace of the policy that steered it (report_schema_version 2.30+). A finding whose effective verdict was actually decided by a selector-scoped rule (not a same-kind overrides: entry) additionally carries its own change.reclassified_by field, naming the deciding rule's label/reason/to (report_schema_version 2.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-relevant escalates only genuinely ABI-relevant drift (std/visibility/packing flags, export policy, toolchain) to API_BREAK; other drift stays a risk.
  • graph_risk_findings — L5 reachability/impact risks. fail → API_BREAK.
  • require_evidence — when a listed layer is true but absent from either the baseline or target side of the compare, an evidence_required_missing finding (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:

internal_namespaces:
  - priv
  - vendor_impl

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) switches compare to severity-based exit codes, where 1 means 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):

id: vendor_sdk_relaxations
version: 1
kind: policy
assignments:
  func_removed: warn

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).