Skip to content

Case 207: Pointer Parameter Gained restrict

Field Value
Verdict 🟡 COMPATIBLE_WITH_RISK
Category Risk
Classification Rule
Platforms Linux
Flags Bad practice
Detected ChangeKinds param_restrict_added
Source files catalog/cases/case207_pointer_parameter_gained_restrict/
Rule family pointer-parameter-gained-restrict
Subject Function-signature and source-API changes

Category: Risk | Verdict: 🟡 COMPATIBLE_WITH_RISK

Verdict and consumer impact

blend()'s two pointer parameters gain restrict. Nothing about the calling convention changes — a float * is still one machine word in the same register — so no prebuilt consumer fails to link and no call site fails to compile. What changed is the caller's obligation: under v1 a caller could legally pass overlapping buffers; under v2 doing so is undefined behaviour, and the callee's compiler is now free to vectorise the loop on that promise.

That obligation is not hypothetical: the runtime witness below shows an unchanged, already-built consumer that relies on overlap computing a different result after the swap, under both GCC and Clang. Consumers that never alias the two arguments are unaffected, and nothing in the library says which kind a consumer is — so the finding is a conditional risk (param_restrict_added, COMPATIBLE_WITH_RISK), not a calling-convention break. A project that promises overlap-tolerant behaviour should gate it.

It is the mirror image of case186_c_api_pointee_const_abi_neutral, where a qualifier change tightened what the callee promises. Here the tightening runs the other way, onto the caller.

Its negative control is case208_restrict_added_to_definition_only, where restrict appears only inside the implementation and never in the published declaration. The pair proves two things about these two fixtures: a restrict change on the published contract is reported, and one confined to the implementation is not — although case208's implementation-only restrict breaks the same overlapping consumer at runtime (recorded there as a separate behavioral break, not a detection target).

Old/new diff

v1.h v2.h
void blend(float *dst, const float *src, int n); void blend(float *restrict dst, const float *restrict src, int n);

abicheck command

gcc -shared -fPIC -g v1.c -o libv1.so
gcc -shared -fPIC -g v2.c -o libv2.so
abicheck compare libv1.so libv2.so --header old=v1.h --header new=v2.h

Expected abicheck finding

Verdict: COMPATIBLE_WITH_RISK (exit 0)

param_restrict_added: Parameter restrict qualifier added: blend param dst
param_restrict_added: Parameter restrict qualifier added: blend param src

func_params_changed must not appear: top-level restrict qualifies the parameter object, not the function's type. (The clang header backend used to keep it in the parameter's type spelling and report a BREAKING "parameters changed" finding with the wrong mechanism; both backends now carry it only through Param.is_restrict.) Removing restrict is reported as param_restrict_changed (COMPATIBLE): the callee only drops an optimizer assumption.

Minimum evidence

min_evidence: L2 — restrict is a declaration property the header-AST backends record (Param.is_restrict). It is not part of the mangled name (L0) and DWARF's own DW_TAG_restrict_type is not what abicheck reads here, so the public header is the evidence tier this finding rests on.

Why abicheck catches it

compare/parameter_facts.py compares each matched parameter's is_restrict_fact between the two sides and emits param_restrict_added when it was gained (param_restrict_changed when lost). It compares the Fact[bool] sibling rather than the raw flag precisely so "not collected" and "confirmed not restrict-qualified" are not folded together — a snapshot produced by a backend that never populated the field would otherwise read as every qualifier having just been added.

Runtime failure demonstration

Severity: wrong results for consumers that pass overlapping buffers.

Both libraries are built at -O3 regardless of the build type (the promise only matters once the optimizer acts on it). The unchanged old consumer computes an in-place running prefix sum, blend(buf + 1, buf, 128), which v1's declaration permits, and checks every element against a sequential oracle.

gcc -O3 -shared -fPIC -g v1.c -o libv1.so
gcc -g app.c -L. -lv1 -Wl,-rpath,'$ORIGIN' -o app
./app            # first five: 1 3 6 10 15                         exit 0
gcc -O3 -shared -fPIC -g v2.c -o libv1.so   # swap in v2, no recompile
./app            # first five: 1 3 5 7 9  WRONG RESULT at element 2  exit 1

Reproduced with GCC 13 and Clang 18/19, Debug and Release consumers. GCC already takes the vectorised path at 4 elements; Clang needs a longer input, hence 128.

Safe redesign

Do not add restrict to an already-published parameter. Introduce a new entry point that carries the stronger precondition (the case200 pattern) and document the aliasing contract there, leaving the original function's weaker promise intact for existing callers.

Cross-tool comparison

abidiff treats restrict as part of the parameter's type and reports a subtype change; ABICC reports a parameter-type change. abicheck reports it as its own param_restrict_changed kind at COMPATIBLE severity, which keeps the aliasing-contract signal visible without claiming a binary break that did not happen.


Ground-truth provenance

Behavioral break: Demonstrated by the runtime witness: an overlapping old consumer gets a different result after the swap under GCC and Clang at -O3.

Source files

  • CMakeLists.txt
  • app.c
  • v1.c
  • v1.h
  • v2.c
  • v2.h

See also: Compatibility Catalog · All COMPATIBLE_WITH_RISK cases · Category: Risk · Rule: Pointer parameter gained restrict · Subject: Function-signature and source-API changes.