Skip to content

Kani reports VERIFICATION SUCCESSFUL when CBMC runs out of memory while writing its JSON results #4905

Closed

Description

@bardiharborow

If CBMC hits a std::bad_alloc while writing its --json-ui results, it still closes the partial JSON cleanly and exits with status 6. Kani ignores that exit status whenever a result array is present. It computes the verdict from a truncated list of properties. Any failures after the truncation point are dropped, so Kani can report VERIFICATION:- SUCCESSFUL and exit 0 for a harness that has a real memory-safety violation.

I tried this code:

#[kani::proof]
fn check() {
    let p: *const u8;
    {
        let y: u8 = kani::any();
        p = &y as *const u8;
    }
    kani::cover!(true, "reachable");
    // Read through a pointer to a dead local: a memory-safety violation.
    let x = unsafe { *p };
    assert!(x == x);
}

using the following command line invocation:

kani oom.rs

with CBMC made to fail one allocation while it writes its report. This harness is too small to run out of memory for real, so I simulated the failure on macOS with an interposed malloc that returns NULL exactly once, on the FAIL_AT-th call:

// failmalloc.c -- clang -dynamiclib -o failmalloc.dylib failmalloc.c
#include <stdlib.h>
#define DYLD_INTERPOSE(_r, _o) __attribute__((used)) static struct { const void *r; const void *o; } \
  _interpose_##_o __attribute__((section("__DATA,__interpose"))) = { (const void *)&_r, (const void *)&_o };
static unsigned long count = 0, fail_at = 0; static int init = 0;
void *my_malloc(size_t sz) {
  if (!init) { init = 1; const char *e = getenv("FAIL_AT"); fail_at = e ? strtoul(e, 0, 10) : 0; }
  if (++count == fail_at) return NULL;
  return malloc(sz);
}
DYLD_INTERPOSE(my_malloc, malloc)
# wrap/cbmc -- put first on PATH
DYLD_INSERT_LIBRARIES=/path/to/failmalloc.dylib exec /opt/homebrew/bin/cbmc "$@"
FAIL_AT=53600 PATH=$PWD/wrap:$PATH kani oom.rs

(The exact FAIL_AT value depends on the machine and CBMC build. Any value that lands inside CBMC's report output shows the problem.)

with Kani version: main @ c35cb96 (0.68.0), CBMC 6.10.0

I expected to see this happen: verification fails, either with the dead-object dereference failure or with Kani's "CBMC appears to have run out of memory" error.

Instead, this happened: CBMC logs Out of memory and exits with status 6. Kani reports success from the 3 properties it did receive, out of the 12 CBMC checks, and exits 0:

Out of memory
RESULTS:
Check 1: check.cover.1
	 - Status: SATISFIED
	 - Description: "reachable"
	 - Location: oom.rs:8:5 in function check

SUMMARY:
 ** 0 of 0 failed
 ** 1 of 1 cover properties satisfied

VERIFICATION:- SUCCESSFUL

Manual Harness Summary:
Complete - 1 successfully verified harnesses, 0 failures, 1 total.

Without the injected failure, the same harness fails as expected with check.pointer_dereference.4 ("dereference failure: dead object in *p").

Activity

  1. Talha-Dmr commented on Sep 29, 2026

    @Talha-Dmr
    Contributor

    Here is a reproducer that triggers this without fault injection. CBMC runs out of memory for real while writing the JSON trace of a property that involves a large symbolic-size allocation.

    // false_success.rs
    // The assertion below is false, yet Kani reports VERIFICATION:- SUCCESSFUL.
    #[kani::proof]
    fn copy_preserves_byte_mutant() {
        let n: usize = kani::any_where(|n: &usize| *n >= (1usize << 32) && *n <= (1usize << 46));
        let k: usize = kani::any_where(|k: &usize| *k < n);
        let src: Vec<u8> = Vec::with_capacity(n);
        let mut dst: Vec<u8> = Vec::with_capacity(n);
        unsafe { core::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), n) };
        let a = unsafe { *src.as_ptr().add(k) };
        let b = unsafe { *dst.as_ptr().add(k) };
        assert!(a != b); // false: the copy preserves byte k
    }
    kani false_success.rs
    

    Kani 0.67.0 built from source at 152c6a8, CBMC 6.10.0, Arch Linux x86_64. It takes about 1 s and peaks at about 160 MiB RSS, so no memory limit is needed to trigger it.

    Kani output (abridged):

    Out of memory
    
    RESULTS:
    ...
    SUMMARY:
     ** 0 of 24 failed (3 unreachable)
    
    VERIFICATION:- SUCCESSFUL
    
    Manual Harness Summary:
    Complete - 1 successfully verified harnesses, 0 failures, 1 total.
    

    Plain CBMC on the same goto binary (kani --keep-temps, then Kani's own CBMC flags without --json-ui) reports the failure:

    cbmc --no-malloc-may-fail --no-undefined-shift-check --no-signed-overflow-check --nan-check \
         --no-self-loops-to-assumptions --no-pointer-primitive-check --object-bits 16 \
         --sat-solver cadical --slice-formula false_success__<...>copy_preserves_byte_mutant.out
    ...
    [copy_preserves_byte_mutant.assertion.1] line 11 [...] assertion failed: a != b: FAILURE
    ** 7 of 253 failed (4 iterations)
    VERIFICATION FAILED
    

    (exit code 10). With --json-ui added, CBMC exits with status 6. Its result array stops after 38 of the 253 properties, followed by {"messageText": "Out of memory", "messageType": "ERROR"}. The last property in the array is copy_preserves_byte_mutant.reachability_check.1 (status FAILURE, meaning reachable). Its trace ends partway through a step inside CBMC's memcpy model (<builtin-library-memcpy>). copy_preserves_byte_mutant.assertion.1 never appears in the array, so Kani reports success from the 38 properties it received.

    We hit this on real harnesses, not only this toy. Verifying alloc::string::String::split_off over strings of arbitrary length, a deliberately false mutant assertion got 0 of 55 failed / VERIFICATION:- SUCCESSFUL from Kani, while plain CBMC on the same goto binary reported the failure. Any harness that copies a large symbolic-size buffer (Vec/String of unbounded length) can hit this. Kani's reachability checks and cover properties also emit traces, so the truncation can happen even when every assertion holds.

    The workaround we use is to treat Kani's verdict as advisory for such harnesses. We run plain CBMC (no --json-ui) on the --keep-temps goto binary and ignore reachability_check/cover FAILUREs.

    A fix that checks CBMC's exit status (at least status 6) when a result array is present, as the issue proposes, would turn this into a clear failure.

  2. self-assigned this
    on Sep 29, 2026
  3. added theissue type on Sep 29, 2026
  4. added a commit that references this issue on Sep 30, 2026
    16c774c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

[C] BugThis is a bug. Something isn't working.

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions