Skip to content

wallet, utils, headersdb: put null guards above the dereference - #404

Open
xanimo wants to merge 1 commit into
dogecoinfoundation:0.1.5-devfrom
xanimo:0.1.5-dev-null-guards
Open

wallet, utils, headersdb: put null guards above the dereference#404
xanimo wants to merge 1 commit into
dogecoinfoundation:0.1.5-devfrom
xanimo:0.1.5-dev-null-guards

Conversation

@xanimo

@xanimo xanimo commented Aug 6, 2026

Copy link
Copy Markdown
Member

Three places check for NULL after already dereferencing the thing they're checking. cppcheck reports all three, and its wording is exact: either the condition is redundant, or the dereference above it can be NULL. It's the latter in all three cases.

Two are certain crashes on ordinary input.

1. dogecoin_wallet_is_from_me — guard four lines too late

if (dogecoin_wallet_get_debit_tx(wallet, tx) > 0)   /* derefs tx->vin */
    return true;

if (!wallet || !tx || !tx->vin)                     /* ...checked here */
    return false;

get_debit_tx opens with if (tx->vin), so a NULL tx crashes before reaching the guard.

Both functions are LIBDOGECOIN_API. rest.c:110 and rest.c:170 pass wtx->tx in without checking it, while wallet.c:954 guards properly — which is why this survived. get_debit_tx gets its own guard too, since rest.c:184 calls it directly.

2. print_header — falls through a failed fopen

if ((fptr = fopen(filename, "r")) == NULL) {
    fprintf(stderr, "error opening %s\n", filename);
}                     /* no return */
print_image(fptr);    /* fgets(..., NULL) */
fclose(fptr);         /* fclose(NULL) — UB */

A file that isn't there is ordinary input, so this crashes on ordinary input. print_image is exported too, so it now rejects a NULL FILE* on its own.

3. headersdb_file.cscan_tip used before its check

scan_tip = db->chaintip then scan_tip->prev at the top of the loop, while the scan_tip && test sits further down the body. chaintip is NULL until the first header connects. The loop condition now carries the check.

Verified, not assumed

Each fix was reverted individually to confirm the new tests catch a real crash rather than merely passing:

Reverted Result
wallet guard AddressSanitizer: SEGV on unknown address 0x000000000008 — the offset of tx->vin
utils guard AddressSanitizer: SEGV on unknown address 0x000000000000

Restored: 82/82, clean under ASan.


Found while triaging the cppcheck backlog. Worth noting this is the same shape as three other things in this codebase: a guard that exists, looks right, and doesn't run — assert under NDEBUG in #382 and qr.c (#401), and now a check placed below its own dereference. Suggests the guards need to be reachable, not just present.

Three places check for NULL after already dereferencing the thing they
are checking. cppcheck reports all three (nullPointerRedundantCheck and
ctunullpointer); its message is exact -- either the condition is
redundant or the dereference above it can be NULL.

dogecoin_wallet_is_from_me() called dogecoin_wallet_get_debit_tx() and
only then checked `!wallet || !tx || !tx->vin`. get_debit_tx() opens with
`if (tx->vin)`, so a NULL tx crashed four lines before the guard that
exists to catch it. Both functions are LIBDOGECOIN_API. rest.c:110 and
rest.c:170 pass wtx->tx straight in without checking it; wallet.c:954
does check, which is why this went unnoticed. The guard moves above the
call, and get_debit_tx() gets its own, since it is exported and rest.c:184
calls it directly.

print_header() ignored a failed fopen. The error branch printed a message
and fell through to print_image(), which ran fgets() on a NULL FILE*, and
then to fclose(NULL). Opening a file that is not there is ordinary input,
so this crashed on ordinary input. Returns on failure now, and
print_image() -- also exported -- rejects a NULL FILE* of its own accord.

headersdb_file.c took `scan_tip = db->chaintip` and dereferenced
`scan_tip->prev` at the top of the loop, while the `scan_tip &&` test sat
further down the body. chaintip is NULL until the first header connects.
The loop condition now carries the check.

Verified the tests fail without each fix rather than merely passing with
it. Reverting the wallet guard alone gives

    AddressSanitizer: SEGV on unknown address 0x000000000008

which is the offset of tx->vin, and reverting the utils guard alone gives

    AddressSanitizer: SEGV on unknown address 0x000000000000

Restored, 82/82 and clean under ASan.
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