Skip to content

Case 186: C API Pointee const-Qualification Is ABI-Neutral

Field Value
Verdict NO_CHANGE
Category No Change
Platforms Linux
Flags
Detected ChangeKinds
Source files examples/case186_c_api_pointee_const_abi_neutral/

Category: No Change | Verdict: ✅ NO_CHANGE

Verdict and consumer impact

send_buffer()'s parameter changes from char * to const char *. The pointer itself is still one machine word, passed the same way, with the same calling convention — only the pointee's mutability contract tightened (the callee now promises not to write through the pointer). Every existing call site — whether it passes a mutable or already-const buffer — still compiles and links unchanged against v2. No consumer action is required.

Old/new diff

v1.h v2.h
void send_buffer(char *data); void send_buffer(const char *data);

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 --ast-frontend clang

Expected abicheck finding

Verdict: NO_CHANGE (exit 0)

_No ABI changes detected._

func_params_changed is expected not to fire here (it would for an ordinary parameter-type change) — the suppression is the point of the case.

Minimum evidence

min_evidence: L2 — a mechanical type-spelling diff would need only DWARF to see char * vs const char * as differing strings and misreport a break; the public header AST is what lets abicheck recognize the top-level *-plus-const-only shape and suppress the finding correctly rather than merely downgrade it. castxml is the documented default backend for this evidence layer; clang (--ast-frontend clang, used above) is a supported alternative AST frontend for hosts without castxml installed.

Why abicheck catches it

abicheck/name_classification.py's cv_qualifiers_only_differ() recognizes a type-pair shape where both sides have a top-level */& and differ only by const/volatile on or behind it. The call site in diff_symbols.py (_params_differ) skips emitting a finding entirely when it fires — this is fully suppressed, not merely downgraded to a risk tier — because a char * argument implicitly converts to const char * at every call site, so no caller can fail to link or misbehave.

Runtime failure demonstration

Severity: none — verified no observable effect.

# Build old library + app
gcc -shared -fPIC -g v1.c -o libv1.so
gcc -g app.c -L. -lv1 -Wl,-rpath,. -o app
./app
# → sent 5 bytes

# Swap in new library (no recompile)
gcc -shared -fPIC -g v2.c -o libv1.so
./app
# → sent 5 bytes   (identical output, no crash, no misread argument)

The pointer is still passed in the same register with the same width; the callee's added promise not to write through it changes nothing observable to a caller compiled against the old signature.

Safe redesign

None needed — this is the safe pattern. Adding const to a public struct field, by contrast, is not automatically safe: code that writes through the field (buf.data[0] = 'x') would stop compiling against a const-qualified field, a real source-level break despite identical binary layout — that hazard is exactly why this case is scoped to a function parameter rather than a struct field.

Real-world example: Wayland's wl_display accessor functions picked up pointee const on their parameters between releases without an actual ABI break, and conda-forge's libuv 1.5x packaging campaign hit exactly this false-positive class — the motivating case for this suppression.

Cross-tool comparison

A naive AST- or symbol-spelling diff (including some abidiff configurations) reports this as a signature change requiring investigation, since char * and const char * are different type strings; abicheck's header-aware cv_qualifiers_only_differ() check is what tells the two apart from a real pointee-type change (see case46_pointer_chain_type_change for the non-suppressed baseline).

References

  • tests/test_const_pointer_abi_neutral.py — unit-level equivalent
  • tests/test_libuv_private_type_churn.py — the struct-field negative case

Source files

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

See also: Examples overview · All NO_CHANGE cases · Category: No Change.