case155_char_signedness_flip โ Plain-char signedness flip (-fsigned-char โ -funsigned-char)¶
| Field | Value |
|---|---|
| Verdict | ๐ก COMPATIBLE_WITH_RISK |
| Category | Risk |
| Platforms | Linux |
| Flags | Bad practice |
Detected ChangeKinds |
char_signedness_changed |
| Source files | examples/case155_char_signedness_flip/ |
Category: Risk | Verdict: ๐ก COMPATIBLE_WITH_RISK
Verdict and consumer impact¶
v1 and v2 share identical source and identical exported symbols โ v1 was
built with -fsigned-char, v2 with -funsigned-char. char, signed
char, and unsigned char are three distinct types in C/C++; a plain-char
parameter or struct member reinterprets the same bytes with the opposite
sign once a consumer is recompiled against the other setting. Comparisons
(if (c < 0)), sign-extension on assignment to a wider integer, and
printf-style range checks all silently change behavior with no compiler
error and no link error.
Old/new diff¶
| Build flags | v1 | v2 |
|---|---|---|
plain-char signedness |
-fsigned-char |
-funsigned-char |
Source and exported symbols are byte-for-byte identical between v1 and v2; only this compile option differs.
abicheck command¶
The case ships old.json/new.json as hand-built BuildEvidence fixtures
(the normalized L3 model dump --build-info/--sources would produce from a
real build) rather than compiled binaries, so reproducing the finding means
embedding each side's fixture into a snapshot's build_source field โ
exactly what dump --build-info does internally โ and comparing the two:
python3 - <<'PY'
import json
from pathlib import Path
from abicheck.model import AbiSnapshot
from abicheck.buildsource.pack import BuildSourcePack
from abicheck.buildsource.build_evidence import BuildEvidence
from abicheck.serialization import save_snapshot
for side in ("old", "new"):
d = json.loads(Path(f"{side}.json").read_text())
snap = AbiSnapshot(library="libdemo.so", version="1")
snap.build_source = BuildSourcePack(root=Path(""), build_evidence=BuildEvidence.from_dict(d))
save_snapshot(snap, f"{side}.abi.json")
PY
abicheck compare old.abi.json new.abi.json
Expected abicheck finding¶
Verdict: COMPATIBLE_WITH_RISK (exit 0)
Deployment Risk Changes:
- char_signedness_changed: Runtime-model option 'char_signedness' changed: 'signed' -> 'unsigned'.
> May not be link- or runtime-compatible across consumers; the artifact
diff confirms any concrete break.
Minimum evidence¶
min_evidence: L3 โ the compile option itself carries the fact. Plain-char
signedness never appears in the exported symbol table and is not
distinguished in most layout dumps, so only the captured build flag exposes
it.
Why abicheck catches it¶
abicheck compare reads each side's normalized BuildEvidence.build_options
(as embedded by dump --build-info/--sources, or supplied out-of-band via
--old/new-build-info) and diffs the char_signedness option directly โ
the same diff_build_evidence() routine tests/test_l3l4l5_examples.py
exercises against the committed fixtures. Because the platform default for
plain-char signedness is target-dependent (signed on x86, unsigned on
most ARM targets), abicheck requires both sides to state the flag
explicitly before reporting a flip, avoiding a false finding on a project
that merely records its platform default. Per ADR-028 D3 this build-evidence
finding never decides a shipped-ABI break on its own โ it flags the
elevated risk and localizes the cause; an artifact diff of actual observed
values is what would confirm a concrete break.
Runtime failure demonstration¶
There's no compiled app.c consumer for this case โ the failure mode is a
cross-toolchain one, not a single process crash. Picture a library built
for x86_64 (-fsigned-char is GCC's default there) and cross-compiled for
an ARM target where -funsigned-char is the platform default, both from
identical source and shipping identical symbols: a consumer that does
if (c < 0) on a plain-char field behaves correctly on one platform and
never on the other, with no build failure anywhere. A CI job that diffs
captured build options across platform-specific build configurations โ not
just across releases โ is exactly what would catch this.
Safe redesign¶
Pin one char signedness across the library and its consumers (-fsigned-char
or -funsigned-char explicitly, rather than relying on the platform
default), or avoid plain char in public interfaces entirely โ use
signed char/unsigned char (or int8_t/uint8_t) where the sign matters.
Cross-tool comparison¶
abidiff/abi-compliance-checker compare built binaries (symbols + DWARF);
neither reads compile options, and plain-char signedness is not
distinguished as a separate type in DWARF, so two builds from identical
source under different -f{signed,unsigned}-char settings produce no diff
for either tool. Only the L3 build-evidence layer that abicheck reads
directly localizes the cause to the signedness flag flip.
Source files¶
new.jsonold.json
See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.