Summary
The compact_field() function in src/pins/sss/clevis-decrypt-sss.c
reads a JWE compact-serialization field from stdin into a dynamically
grown buffer. The buffer size size is incremented by 4096 with no
overflow guard. In theory, if size wraps around SIZE_MAX, the
subsequent realloc would shrink the buffer while the write index
used continues to grow, causing an out-of-bounds write.
Practical impact — none
This is a code-quality / hardening issue, not a practically
exploitable vulnerability:
- 64-bit systems: the wrap requires ~18 EB of input — physically
impossible.
- 32-bit systems: although
SIZE_MAX is ~4 GiB and the arithmetic
wraps at that point, the 32-bit virtual address space is limited to
~3 GB. The realloc calls will fail and return NULL well before
size can reach the wrap point. The existing check
if (!tmp) goto error; catches the allocation failure, so the
function exits cleanly before the overflow can occur.
The bug is a missing hardening guard — the code is technically
incorrect but is protected in practice by the address-space limit on
32-bit and the input-size impossibility on 64-bit. The fix is trivial
(one line) and worth applying for correctness.
Affected code
src/pins/sss/clevis-decrypt-sss.c, compact_field(), lines 84–107:
static json_t *
compact_field(FILE *file)
{
json_t *str = NULL;
char *buf = NULL;
size_t used = 0;
size_t size = 0;
for (int c = fgetc(file); c != EOF && c != '.' && !isspace(c);
c = fgetc(file)) {
if (used >= size) {
char *tmp = NULL;
size += 4096; /* BUG: no overflow check */
tmp = realloc(buf, size);
if (!tmp)
goto error;
buf = tmp;
}
buf[used++] = c; /* OOB write when size has wrapped */
}
str = json_stringn(buf ? buf : "", buf ? used : 0);
error:
free(buf);
return str;
}
Root cause
The growth step size += 4096 (line 95) performs unsigned integer
addition with no overflow check. When size reaches the largest
4096-aligned value below SIZE_MAX (i.e. SIZE_MAX - 4095 on
32-bit = 0xFFFFF001), the next size += 4096 wraps to
0x00001000 (4096). realloc(buf, 4096) then shrinks the
buffer back to 4096 bytes, while used is approximately 4 billion.
The subsequent buf[used++] = c writes gigabytes past the
allocation.
The invariant used <= size (the buffer has room for the write) is
violated by the wrap. The code has no guard equivalent to
if (size > SIZE_MAX - 4096) goto error;.
Reproducer — standalone C demonstrating the 32-bit overflow
Since the overflow requires ~4 GB of input on real 32-bit hardware,
we demonstrate the bug by simulating compact_field with a
uint32_t size variable that wraps at 32 bits, proving the
arithmetic defect:
/*
* repro_clevis_f2.c — demonstrates the integer overflow in compact_field().
*
* Simulates the 32-bit overflow by using uint32_t for the size variable.
* On real 32-bit systems, size_t IS uint32_t and this is the exact behavior.
*
* Compile: gcc -fsanitize=address -g -o repro_clevis_f2 repro_clevis_f2.c
* Run: ./repro_clevis_f2
* Expected: Demonstrates that size wraps around and realloc shrinks the buffer.
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* Reproduces compact_field's growth logic with uint32_t size
* (simulating a 32-bit size_t where SIZE_MAX == 0xFFFFFFFF).
*
* Instead of actually reading 4 GB of input, we simulate the state
* just before the overflow by directly setting used and size to their
* values at the wrap point.
*/
int main(void)
{
const uint32_t SIZE_MAX_32 = UINT32_MAX; /* 0xFFFFFFFF */
/*
* After reading (SIZE_MAX_32 - 4095) bytes, the state is:
* used = 0xFFFFF001 (just triggered the last successful realloc)
* size = 0xFFFFF000 (largest 4096-aligned value < SIZE_MAX_32)
*
* Wait — let's trace the exact sequence:
* size starts at 0, grows by 4096 each time used >= size.
* After N growths: size = N * 4096.
* The last growth that fits: N = 0xFFFFF000 / 4096 = 0x000FFFFF
* So size = 0xFFFFF000, and the buffer holds up to 0xFFFFF000 bytes.
*
* When used == 0xFFFFF000 (buffer full), the next byte triggers:
* used >= size → true
* size += 4096 → 0xFFFFF000 + 0x1000 = 0x100000000
* which wraps to 0x00000000 on 32-bit!
*
* realloc(buf, 0) → implementation-defined (free or tiny alloc)
* buf[used++] = c → writes at offset 0xFFFFF000 into a freed/tiny buffer
*
* Actually: 0xFFFFF000 + 4096 = 0x100000000 → wraps to 0x0 (32-bit).
* But let's check: does it wrap to 0 or 4096?
* 0xFFFFF000 = 4294963200
* + 4096 = 4294967296 = 2^32 → wraps to 0
*
* realloc(buf, 0) is equivalent to free(buf) on many implementations,
* then buf[used++] = use-after-free + OOB write.
*/
printf("Simulating compact_field on 32-bit (uint32_t size):\n\n");
uint32_t size = 0;
uint32_t used = 0;
/* Simulate growth up to the wrap point */
printf("1. Buffer grows normally: size += 4096 each time used >= size\n");
size = 0xFFFFF000u; /* last successful growth */
used = 0xFFFFF000u; /* buffer is now full */
printf(" After ~4 GB of input: size = 0x%08X, used = 0x%08X\n",
size, used);
/* Now: used >= size triggers growth */
printf("\n2. Next byte: used(0x%08X) >= size(0x%08X) → grow buffer\n",
used, size);
size += 4096;
printf(" size += 4096 → size = 0x%08X *** WRAPPED TO %u ***\n",
size, size);
printf("\n3. realloc(buf, %u) → ", size);
if (size == 0) {
printf("realloc(buf, 0) = free(buf) or tiny alloc!\n");
} else {
printf("realloc(buf, %u) = buffer SHRUNK from ~4 GB to %u bytes!\n",
size, size);
}
printf("\n4. buf[used++] = c → writes at offset 0x%08X (%u)\n",
used, used);
printf(" into a buffer of size %u → OOB WRITE of ~%u GB past allocation\n",
size, (used - size) / (1024*1024*1024));
printf("\n*** INTEGER OVERFLOW CONFIRMED: "
"size wraps from 0xFFFFF000 to 0x%08X ***\n", size);
/*
* Concrete proof with actual memory (small scale):
* Allocate 8192 bytes, simulate the state where size wrapped to 4096.
*/
printf("\n--- Concrete small-scale demo with ASan ---\n");
char *buf = malloc(4096); /* simulates post-realloc(buf, 4096) after wrap */
if (!buf) return 1;
memset(buf, 0, 4096);
/* After wrap: size = 4096 (or 0), used = 0xFFFFF000.
* The code does buf[used++] = c; but used is huge.
* We simulate with a smaller offset that still triggers ASan: */
printf("Writing buf[4096] (one past allocation) to trigger ASan...\n");
buf[4096] = 'X'; /* ASan: heap-buffer-overflow WRITE */
free(buf);
return 0;
}
Expected output
Without ASan (arithmetic proof):
Simulating compact_field on 32-bit (uint32_t size):
1. Buffer grows normally: size += 4096 each time used >= size
After ~4 GB of input: size = 0xFFFFF000, used = 0xFFFFF000
2. Next byte: used(0xFFFFF000) >= size(0xFFFFF000) → grow buffer
size += 4096 → size = 0x00000000 *** WRAPPED TO 0 ***
3. realloc(buf, 0) → realloc(buf, 0) = free(buf) or tiny alloc!
4. buf[used++] = c → writes at offset 0xFFFFF000 (4294963200)
into a buffer of size 0 → OOB WRITE of ~3 GB past allocation
*** INTEGER OVERFLOW CONFIRMED: size wraps from 0xFFFFF000 to 0x00000000 ***
With ASan (heap-buffer-overflow on the small-scale demo):
==PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
WRITE of size 1 at 0x... thread T0
#0 0x... in main repro_clevis_f2.c:104
...
0x... is located 0 bytes after 4096-byte region [0x...,0x...)
SUMMARY: AddressSanitizer: heap-buffer-overflow repro_clevis_f2.c:104 in main
Scope
compact_field() is called from compact_jwe() (line 118), which
parses the JWE compact serialization from stdin. This happens in
main() at line 153 — before any cryptographic verification:
jwe = compact_jwe(stdin); /* ← parses input */
if (!jwe)
goto egress;
The three compact_field calls parse the protected, encrypted_key,
and iv fields.
Fix
Add an overflow guard before the size increment:
--- a/src/pins/sss/clevis-decrypt-sss.c
+++ b/src/pins/sss/clevis-decrypt-sss.c
@@ -93,6 +93,8 @@
if (used >= size) {
char *tmp = NULL;
+ if (size > SIZE_MAX - 4096)
+ goto error;
size += 4096;
tmp = realloc(buf, size);
if (!tmp)
How this was found
Found by applying the Squeeze Loop strategy ("The Squeeze Loop
Strategy: Catching Coherent-and-Wrong Artifacts with an
Author-Independent Executable Oracle," Zenodo
10.5281/zenodo.20787816,
2026) on its C terrain. The WP analysis surfaced this not via a
default RTE alarm (unsigned integer wrap is defined behavior in C),
but through the non-inductive loop invariant used <= size
(range_preserved) — the functional bound that ensures the buffer
has room for each write. This invariant fails to discharge because
size += 4096 can wrap, breaking the used <= size relationship.
A refutation probe inserting assert size <= SIZE_MAX - 4096 before
size += 4096 causes the invariant to discharge and the assert
becomes the sole residual — localizing the failure exactly to the
missing overflow guard, not prover weakness. The guard that makes the
proof green is the fix.
Summary
The
compact_field()function insrc/pins/sss/clevis-decrypt-sss.creads a JWE compact-serialization field from stdin into a dynamically
grown buffer. The buffer size
sizeis incremented by 4096 with nooverflow guard. In theory, if
sizewraps aroundSIZE_MAX, thesubsequent
reallocwould shrink the buffer while the write indexusedcontinues to grow, causing an out-of-bounds write.Practical impact — none
This is a code-quality / hardening issue, not a practically
exploitable vulnerability:
impossible.
SIZE_MAXis ~4 GiB and the arithmeticwraps at that point, the 32-bit virtual address space is limited to
~3 GB. The
realloccalls will fail and return NULL well beforesizecan reach the wrap point. The existing checkif (!tmp) goto error;catches the allocation failure, so thefunction exits cleanly before the overflow can occur.
The bug is a missing hardening guard — the code is technically
incorrect but is protected in practice by the address-space limit on
32-bit and the input-size impossibility on 64-bit. The fix is trivial
(one line) and worth applying for correctness.
Affected code
src/pins/sss/clevis-decrypt-sss.c,compact_field(), lines 84–107:Root cause
The growth step
size += 4096(line 95) performs unsigned integeraddition with no overflow check. When
sizereaches the largest4096-aligned value below
SIZE_MAX(i.e.SIZE_MAX - 4095on32-bit =
0xFFFFF001), the nextsize += 4096wraps to0x00001000(4096).realloc(buf, 4096)then shrinks thebuffer back to 4096 bytes, while
usedis approximately 4 billion.The subsequent
buf[used++] = cwrites gigabytes past theallocation.
The invariant
used <= size(the buffer has room for the write) isviolated by the wrap. The code has no guard equivalent to
if (size > SIZE_MAX - 4096) goto error;.Reproducer — standalone C demonstrating the 32-bit overflow
Since the overflow requires ~4 GB of input on real 32-bit hardware,
we demonstrate the bug by simulating
compact_fieldwith auint32_tsize variable that wraps at 32 bits, proving thearithmetic defect:
Expected output
Without ASan (arithmetic proof):
With ASan (heap-buffer-overflow on the small-scale demo):
Scope
compact_field()is called fromcompact_jwe()(line 118), whichparses the JWE compact serialization from stdin. This happens in
main()at line 153 — before any cryptographic verification:The three
compact_fieldcalls parse theprotected,encrypted_key,and
ivfields.Fix
Add an overflow guard before the size increment:
How this was found
Found by applying the Squeeze Loop strategy ("The Squeeze Loop
Strategy: Catching Coherent-and-Wrong Artifacts with an
Author-Independent Executable Oracle," Zenodo
10.5281/zenodo.20787816,
2026) on its C terrain. The WP analysis surfaced this not via a
default RTE alarm (unsigned integer wrap is defined behavior in C),
but through the non-inductive loop invariant
used <= size(
range_preserved) — the functional bound that ensures the bufferhas room for each write. This invariant fails to discharge because
size += 4096can wrap, breaking theused <= sizerelationship.A refutation probe inserting
assert size <= SIZE_MAX - 4096beforesize += 4096causes the invariant to discharge and the assertbecomes the sole residual — localizing the failure exactly to the
missing overflow guard, not prover weakness. The guard that makes the
proof green is the fix.