Skip to content

linux: fix canister reverse-relocation section numbering - #33

Closed
dcasota wants to merge 1 commit into
5.0from
fix/canister-relocs-section-index
Closed

dcasota wants to merge 1 commit into
5.0from
fix/canister-relocs-section-index

Conversation

@dcasota

@dcasota dcasota commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Problem

A kernel that links a locally built FIPS canister panics at 0.64 s:

FIPS(fips_integrity_init): canister 6.12 found (based on 6.12.109-4.ph5)
FIPS(fips_integrity_init): processing 8 sections, 687696 bytes
Kernel panic - not syncing: FIPS canister verification failed!
   fips_integrity_check+0x1a1/0x2e8

The canister that ships with the distribution (6.12.60-18.2) still verifies, so FIPS boot as such is fine. Only canisters built from current sources fail — which is what canister_build=1 produces.

Root cause

gen_canister_relocs gives each section an ondx, the index fips_integrity_init() uses to address its si[] array when reversing a relocation:

r->section = section_symbols[sndx]->ondx;   /* gen_canister_relocs.c:292 */
pc  = si[r->section].saddr + offset;        /* fips_integrity.c:349 */

si[] is built from canister_sections[], which by construction holds only sections carrying both markers:

if (section_symbols[i] && section_symbols[i]->end)   /* gen_canister_relocs.c:430 */

.bss carries a begin marker only — it is deliberately not measured, it exists in the list purely so relocations can resolve against it — yet it still consumed an ondx. Every section laid out after .bss was therefore numbered one too high, and its relocations were reversed against the wrong section's buffer. The reconstructed image then no longer matched the HMAC recorded at build time.

This stayed invisible for as long as .bss happened to be emitted after every measured section. gcc 12.2 did exactly that; gcc 12.5 (5fce7ef, 92: core-toolchain: upgrade gcc to 12.5 and binutils to 2.46.1) does not:

section order (ondx 0…)
gcc 12.2 .text .init.text .exit.text .rodata .smp_locks .data .init.data .bss .init.rodata
gcc 12.5 .rodata .text .data .bss .init.text .exit.text .smp_locks .init.data .init.rodata

Under the new layout .init.text, .exit.text, .smp_locks and .init.data568 relocations — are all misdirected, and ondx reaches 8 on an eight-entry si[].

.init.rodata was already numbered one too high under gcc 12.2 as well. It never mattered because it carries zero relocations, in both the passing and the failing canister.

Fix

Only a section that is actually measured may consume an index. .bss cannot hold relocations (it is NOBITS), so the sentinel is never dereferenced.

The measured set, the generated linker script and therefore the canister HMAC are all unchanged — running the old and the new generator over the same object produces a byte-identical canister_markers.lds and an identical canister_sections_size. Only the numbering handed to the interpreter changes.

Evidence

Established from build artefacts, without rebuilding:

  • The compiler is the trigger. The last canister that verified reports GCC: (GNU) 12.2.0 in .comment; the first that failed reports GCC: (GNU) 12.5.0. Same kernel source version on both sides of the boundary, so the kernel version is not the variable.
  • The build side is correct. Recomputing the build-time measurement from each shipped fips_canister.oobjcopy --only-section per section in linker-script order, concatenated, HMAC-SHA256 — reproduces the injected digest exactly, for the passing and the failing canister alike.
  • The linker is correct. Every byte that differs between the canister object and the same region in the linked vmlinux is covered by a relocation in that section; there are zero uncovered differences across all eight measured sections. Nothing beyond relocation application touches the canister.

That leaves the reverse-relocation step, and the numbering above accounts for it.

section size differing vs vmlinux relocs uncovered
.text 249367 14105 3974 0
.init.text 6264 1475 378 0
.exit.text 1510 385 100 0
.rodata 345384 33975 4278 0
.smp_locks 8 8 2 0
.data 39976 2081 262 0
.init.data 704 702 88 0
.init.rodata 44483 0 0 0

Testing

check_spec.py passes; the generator compiles clean with -Wall.

End-to-end confirmation requires a canister build plus a relinked kernel booted with fips=1, which is running separately; this PR is posted on the strength of the artefact analysis above rather than waiting on it.

Note on certification

A canister built this way is functionally correct but not CMVP-validated — the published 6.12.60-18.2 cannot certify a 6.12.109 kernel, and nothing built locally certifies anything. This fix is about a locally built canister being correct, which is what the boot-time integrity check demands regardless of certification.

🤖 Generated with Claude Code

Booting a kernel that links a locally built FIPS canister panics early:

  FIPS(fips_integrity_init): canister 6.12 found (based on 6.12.109-4.ph5)
  FIPS(fips_integrity_init): processing 8 sections, 687696 bytes
  Kernel panic - not syncing: FIPS canister verification failed!

gen_canister_relocs assigns every measured section an "ondx", the index
fips_integrity_init() uses to address its si[] array when it reverses a
relocation. si[] is built from canister_sections[], which contains only
the sections that carry both a begin and an end marker. .bss carries a
begin marker only - it is deliberately not measured - yet it still
consumed an ondx. Every section laid out after .bss therefore received
an index one too high, and its relocations were reversed against the
wrong section's buffer, so the reconstructed image no longer matched the
HMAC recorded at canister build time.

The bug was latent for as long as .bss happened to be emitted after all
measured sections, which is what gcc 12.2 did. gcc 12.5 (5fce7ef)
emits .bss in the middle:

  gcc 12.2   .text .init.text .exit.text .rodata .smp_locks .data
             .init.data .bss .init.rodata
  gcc 12.5   .rodata .text .data .bss .init.text .exit.text .smp_locks
             .init.data .init.rodata

With that layout .init.text, .exit.text, .smp_locks and .init.data - 568
relocations - are all misdirected, and ondx reaches 8 on an eight entry
si[] array.

Only sections that are actually measured may consume an index. The
measured set, the generated linker script and therefore the canister
HMAC itself are unchanged by this; only the numbering handed to the
interpreter changes.

Verified from the build artefacts: the canister that last passed
verification reports GCC 12.2.0 in .comment and the first that failed
reports GCC 12.5.0; recomputing the build-time HMAC from each shipped
fips_canister.o reproduces the injected digest exactly, and every byte
that differs between the canister object and the linked vmlinux is
covered by a relocation. The defect is confined to the reverse
relocation step.

Change-Id: Ia09d27e079e0dd52f6ed3e47bf44e8a43ed6ae53
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW73JTCUGRcaNTUEQcAMtf
@dcasota

dcasota commented Sep 13, 2026

Copy link
Copy Markdown
Owner Author

Folded into #29, which carries the same change as 6.12.109-4. Keeping it separate would have collided with #24 and #29 on the linux.spec Release line, and it belongs with them in any case: a canister that builds but is rejected at boot is the same defect this series addresses.

@dcasota dcasota closed this Sep 13, 2026
@dcasota
dcasota deleted the fix/canister-relocs-section-index branch September 13, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant