Merge pull request #556 from LedgerHQ/mem_opt
What changed, and why it matters
This commit removes an old memory workaround in Ledger's Bitcoin app. Previously, a large data structure used during transaction signing was stored in global memory instead of on the function's stack, because some Ledger devices were thought to have only 8 KB of stack space. The update increases the allowed stack size for the Nano X and moves that data structure back onto the stack. This is a routine code cleanup, but it changes how memory is allocated during the most memory-intensive signing operation. If the new stack limit is slightly wrong, it could in theory cause stack overflows or memory corruption during signing on a Nano X.
Treat as a defensive review item. Verify that the Nano X stack size estimate (12288 bytes) safely exceeds actual peak stack consumption during PSBT signing, especially with MuSig2 and the maximum number of external outputs. Run stack-usage analysis and fuzzing on Nano X builds. If the stack guard is too optimistic, a stack overflow could corrupt memory or crash the device during signing.
Security signals we found
Memory allocation model changed for high-risk signing path
Stack-size build-time guard changed for Nano X
Global cache removed; signing state now lives on stack
No explicit security bug or CVE mentioned in commit
No bounds checks or overflow mitigations added in diff
Evidence from the diff
The patch removes the global sign_psbt_cache_t G_sign_psbt_cache and instead declares it as a local variable (on the stack) inside handler_sign_psbt(). It also raises APP_STACK_MIN_SIZE for Nano X from 8192 to 12288 and updates comments to say Nano X has smaller RAM rather than an 8 KB stack limit. Several cx_* internal headers are removed from crypto.c, and the fuzzing invariant list no longer expects the global cache symbol. The change is framed as removing an obsolete BOLOS workaround.
Changed components
src/handler/sign_psbt.csrc/constants.hMakefilesrc/crypto.cfuzzing/invariants/zero-symbols.txtInspect captured patch +9 / −27
### Makefile
@@ -170,9 +170,9 @@ DEFINES += HAVE_BOLOS_APP_STACK_CANARY
# Estimated maximum stack usage.
# It acts as a build-time check to protect global variables.
-# On the Nano X, the stack size is limited to 8K at the OS level.
+# On the Nano X, the RAM size is smaller, so we keep a smaller stack size.
ifeq ($(TARGET_NAME),TARGET_NANOX)
- APP_STACK_MIN_SIZE := 8192
+ APP_STACK_MIN_SIZE := 12288
else
APP_STACK_MIN_SIZE := 16384
endif
@@ -200,9 +200,6 @@ ifeq ($(DEBUG),10)
DEFINES += HAVE_SEMIHOSTED_PRINTF
endif
-# Needed to be able to include the definition of G_cx
-INCLUDES_PATH += $(BOLOS_SDK)/lib_cxng/src
-
########################################
# Features enablers #
########################################
### fuzzing/invariants/zero-symbols.txt
@@ -7,7 +7,6 @@ G_output_len
G_dispatcher_state
G_dispatcher_context
g_ui_state
-G_sign_psbt_cache
pairs
# Pointers overwritten by the harness every iteration (random bytes crash).
### src/constants.h
@@ -57,13 +57,12 @@
/**
* Maximum number of external outputs handled simultaneously.
- * On the Nano X, the stack size is limited to 8K at the OS level,
- * so the stack consumption has to be limited as well.
*/
#ifndef TARGET_ID
#error "bolos_target.h must be included (TARGET_* constants unavailable)"
#endif
#ifdef TARGET_NANOX
+// Nano X has smaller RAM size, so we keep this smaller.
#define MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER 8U
#else
#define MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER 16U
### src/crypto.c
@@ -26,14 +26,8 @@
#include "bip32.h"
#include "crypto_helpers.h"
#include "cx.h"
-#include "cx_ecfp.h"
-#include "cx_ram.h"
-#include "cx_ripemd160.h"
-#include "cx_stubs.h"
#include "format.h"
-#include "lcx_ripemd160.h"
#include "os.h"
-#include "ox_ec.h"
#include "read.h"
#include "write.h"
### src/handler/sign_psbt.c
@@ -41,13 +41,6 @@
#include "sw.h"
#include "txhashes.h"
-// We declare this in the global space in order to use less stack space, since BOLOS enforces on
-// some devices an 8kb stack limit.
-// Once this is resolved in BOLOS, we should move this to the function scope to avoid unnecessarily
-// reserving RAM that can only be used for the signing flow (which, at time of writing, is the most
-// RAM-intensive operation command of the app).
-sign_psbt_cache_t G_sign_psbt_cache;
-
void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) {
LOG_PROCESSOR(__FILE__, __LINE__, __func__);
@@ -62,8 +55,8 @@ void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) {
// read APDU inputs, initialize global state and read global PSBT map
if (!init_global_state(dc, &st)) return;
- sign_psbt_cache_t *cache = &G_sign_psbt_cache;
- init_sign_psbt_cache(cache);
+ sign_psbt_cache_t cache;
+ init_sign_psbt_cache(&cache);
// bitmap to keep track of which inputs are internal
uint8_t internal_inputs[BITVECTOR_REAL_SIZE(MAX_N_INPUTS_CAN_SIGN)];
@@ -81,14 +74,14 @@ void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) {
* - detect internal inputs that should be signed, and if there are external inputs or unusual
* sighashes
*/
- if (!preprocess_inputs(dc, &st, cache, internal_inputs)) return;
+ if (!preprocess_inputs(dc, &st, &cache, internal_inputs)) return;
/** OUTPUTS VERIFICATION FLOW
*
* For each output, check if it's a change address.
* Check if it's an acceptable output.
*/
- if (!preprocess_outputs(dc, &st, cache, internal_outputs)) return;
+ if (!preprocess_outputs(dc, &st, &cache, internal_outputs)) return;
// check if we're only executing the MuSig2 Round 1
bool only_signing_for_musig = true;
@@ -117,7 +110,7 @@ void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) {
// pubnonces; this does not involve the private keys, therefore we can do it without user
// confirmation
- if (!produce_musig2_pubnonces(dc, &st, &signing_state, cache, internal_inputs)) {
+ if (!produce_musig2_pubnonces(dc, &st, &signing_state, &cache, internal_inputs)) {
return;
}
}
@@ -152,7 +145,7 @@ void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) {
* For each internal key expression, and for each internal input, sign using the
* appropriate algorithm.
*/
- int sign_result = sign_transaction(dc, &st, cache, &signing_state, internal_inputs);
+ int sign_result = sign_transaction(dc, &st, &cache, &signing_state, internal_inputs);
#ifdef HAVE_SWAP
if (!G_called_from_swap)Why this scored 32/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.