psbt: handle allocation failures in set_global_tx
What changed, and why it matters
This commit fixes a memory-management bug in the library's Partially Signed Bitcoin Transaction (PSBT) handling. When the code failed to allocate memory for new transaction inputs or outputs, it could leave behind partially initialized data and might not clean up old input/output records properly. The patch makes sure initialization only happens if memory was actually allocated, and that any old or partially built data is fully freed before returning an error. In practice this is mainly a reliability/hardening fix against out-of-memory conditions rather than a directly exploitable attack path.
Treat as a defensive hardening patch. Reviewers should verify that all callers of psbt_set_global_tx() propagate WALLY_ENOMEM correctly and that no other allocation-failure paths in the PSBT code leave partially initialized structures. Consider backporting to stable branches if the library is used in long-running or resource-constrained services.
Security signals we found
Allocation-failure handling added (NULL checks before array initialization)
Resource cleanup improved (element-level free before array free)
Potential use of uninitialized/freed memory path removed in OOM case
No explicit security advisory, CVE, or attribution in commit
Evidence from the diff
In src/psbt.c, psbt_set_global_tx() now checks that wally_malloc() succeeded before calling psbt_input_init()/psbt_output_init() on the new arrays. It also replaces bare wally_free() calls with new psbt_inputs_free()/psbt_outputs_free() helpers that first call psbt_input_free()/psbt_output_free() on each element. This prevents two problems: (1) initializing array slots in NULL memory when allocation fails, and (2) leaking or leaving unfreed internal fields of the old psbt->inputs/outputs arrays when they are replaced. The same helpers are reused in wally_psbt_free() for consistency. The change is defensive and improves behavior under OOM, but it does not appear to introduce or fix a remote-exploitable vulnerability on its own.
Changed components
src/psbt.cwally_psbt_free()psbt_set_global_tx()PSBT input/output allocation and deallocation pathsInspect captured patch +26 / −14
diff --git a/src/psbt.c b/src/psbt.c
index 74b46a5..5806aab 100644
--- a/src/psbt.c
+++ b/src/psbt.c
@@ -868,6 +868,15 @@ static int psbt_input_free(struct wally_psbt_input *input, bool free_parent)
return WALLY_OK;
}
+static void psbt_inputs_free(struct wally_psbt_input *inputs, size_t num_inputs)
+{
+ if (inputs) {
+ for (size_t i = 0; i < num_inputs; ++i)
+ psbt_input_free(&inputs[i], false);
+ wally_free(inputs);
+ }
+}
+
MAP_INNER_FIELD(output, redeem_script, PSBT_OUT_REDEEM_SCRIPT, psbt_fields)
MAP_INNER_FIELD(output, witness_script, PSBT_OUT_WITNESS_SCRIPT, psbt_fields)
MAP_INNER_FIELD(output, taproot_internal_key, PSBT_OUT_TAP_INTERNAL_KEY, psbt_fields)
@@ -1149,6 +1158,15 @@ static int psbt_output_free(struct wally_psbt_output *output, bool free_parent)
return WALLY_OK;
}
+static void psbt_outputs_free(struct wally_psbt_output *outputs, size_t num_outputs)
+{
+ if (outputs) {
+ for (size_t i = 0; i < num_outputs; ++i)
+ psbt_output_free(&outputs[i], false);
+ wally_free(outputs);
+ }
+}
+
static int psbt_init(uint32_t version, size_t num_inputs, size_t num_outputs,
size_t num_unknowns, uint32_t flags,
size_t max_num_inputs, size_t max_num_outputs,
@@ -1291,17 +1309,11 @@ static void psbt_claim_allocated_inputs(struct wally_psbt *psbt, size_t num_inpu
int wally_psbt_free(struct wally_psbt *psbt)
{
- size_t i;
if (psbt) {
wally_tx_free(psbt->tx);
- for (i = 0; i < psbt->num_inputs; ++i)
- psbt_input_free(&psbt->inputs[i], false);
+ psbt_inputs_free(psbt->inputs, psbt->num_inputs);
+ psbt_outputs_free(psbt->outputs, psbt->num_outputs);
- wally_free(psbt->inputs);
- for (i = 0; i < psbt->num_outputs; ++i)
- psbt_output_free(&psbt->outputs[i], false);
-
- wally_free(psbt->outputs);
wally_map_clear(&psbt->unknowns);
wally_map_clear(&psbt->global_xpubs);
#ifdef BUILD_ELEMENTS
@@ -1591,31 +1603,31 @@ static int psbt_set_global_tx(struct wally_psbt *psbt, struct wally_tx *tx, bool
if (psbt->inputs_allocation_len < tx->num_inputs) {
new_inputs = wally_malloc(tx->num_inputs * sizeof(struct wally_psbt_input));
- for (i = 0; i < tx->num_inputs; ++i)
+ for (i = 0; new_inputs && i < tx->num_inputs; ++i)
psbt_input_init(&new_inputs[i]);
}
if (psbt->outputs_allocation_len < tx->num_outputs) {
new_outputs = wally_malloc(tx->num_outputs * sizeof(struct wally_psbt_output));
- for (i = 0; i < tx->num_outputs; ++i)
+ for (i = 0; new_outputs && i < tx->num_outputs; ++i)
psbt_output_init(&new_outputs[i]);
}
if ((psbt->inputs_allocation_len < tx->num_inputs && !new_inputs) ||
(psbt->outputs_allocation_len < tx->num_outputs && !new_outputs)) {
- wally_free(new_inputs);
- wally_free(new_outputs);
+ psbt_inputs_free(new_inputs, tx->num_inputs);
+ psbt_outputs_free(new_outputs, tx->num_outputs);
wally_tx_free(new_tx);
return WALLY_ENOMEM;
}
if (new_inputs) {
- wally_free(psbt->inputs);
+ psbt_inputs_free(psbt->inputs, psbt->num_inputs);
psbt->inputs = new_inputs;
psbt->inputs_allocation_len = tx->num_inputs;
}
if (new_outputs) {
- wally_free(psbt->outputs);
+ psbt_outputs_free(psbt->outputs, psbt->num_outputs);
psbt->outputs = new_outputs;
psbt->outputs_allocation_len = tx->num_outputs;
}
Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.