Merge pull request #539 from LedgerHQ/fix-stack-exhaustion
What changed, and why it matters
This update fixes a stack-exhaustion weakness in Ledger's Bitcoin app. A malicious or unusually crafted wallet policy (the text string that describes how coins can be spent) could make the app recurse so deeply that it runs out of call stack and crashes. The patch adds depth limits during parsing, reduces large on-stack buffers, and moves big buffers out of recursive functions so the app rejects oversized policies safely instead of crashing.
Treat this as a security fix and include it in the next firmware/app release. Ensure the new depth limits are enforced consistently across all policy-walking code paths and that fuzzing targets exercise deep wrapper chains and nested thresh expressions.
Security signals we found
Stack exhaustion / unbounded recursion in policy parsing and validation
Missing depth accounting for miniscript wrapper chains
Large automatic arrays inside recursive functions (compute_thresh_ops / compute_thresh_stacksize)
Large handler buffers kept live during recursive policy walks
New explicit limits: MAX_PARSE_SCRIPT_RECURSION_DEPTH 16, MAX_THRESH_NESTING 4, MAX_N_IN_THRESH 24
Evidence from the diff
The commit addresses stack exhaustion in the Bitcoin app policy parser and validators. Key changes: (1) wrapper chains such as wsh(nnn…n:pk(@0/**)) are now counted against MAX_PARSE_SCRIPT_RECURSION_DEPTH because each wrapper adds an AST level; (2) nested thresh() operators are limited to MAX_THRESH_NESTING and charged per nesting level via context flags; (3) compute_thresh_ops and compute_thresh_stacksize are marked noinline so their large local arrays are not duplicated in every frame of recursive callers, and MAX_N_IN_THRESH is reduced from 128 to 24; (4) handler_register_wallet’s large key-info buffers are moved into a separate noinline function so they are not reserved while the recursive policy sanity checks run. Tests are added for deep wrapper chains, thresh nesting, and wide thresh branches.
Changed components
src/common/cleartext.csrc/common/wallet.csrc/common/wallet.hsrc/handler/lib/policy.csrc/handler/register_wallet.ctests/test_register_wallet.pyunit-tests/test_wallet.cInspect captured patch +371 / −142
### src/common/cleartext.c
@@ -125,15 +125,6 @@ static uint8_t admitting_pattern_count(const cleartext_spec_t *spec, const ct_bi
// but in practice ≤ ~30 occurrences).
#define CT_MAX_KEYEXPRS 32
-typedef struct {
- // Canonical identity: the first key expression seen in the group. Equality
- // is determined via are_key_placeholders_identical (from policy.h).
- const policy_node_keyexpr_t *repr;
- // Derivation pairs collected for this class.
- uint32_t pairs[CT_MAX_KEYEXPRS][2];
- uint8_t n_pairs;
-} ct_keyexpr_class_t;
-
// Saturating factorial.
static uint64_t sat_factorial(uint32_t n) {
uint64_t f = 1;
@@ -183,60 +174,62 @@ static uint64_t key_orderings_count(const policy_node_t *root, bool *out_canonic
kx[i] = k;
}
- // Group by identity. classes[i].repr is the first keyexpr in the group;
- // classes[i].pairs collects (num_first, num_second) for each occurrence.
- ct_keyexpr_class_t classes[CT_MAX_KEYEXPRS];
+ // Group by identity: class_of[i] is the class of kx[i], and class_repr[c] is the index of the
+ // first keyexpr seen in class c (its canonical identity). Equality is determined via
+ // are_key_placeholders_identical (from policy.h).
+ uint8_t class_of[CT_MAX_KEYEXPRS];
+ uint8_t class_repr[CT_MAX_KEYEXPRS];
int n_classes = 0;
for (int i = 0; i < n; i++) {
- const policy_node_keyexpr_t *k = kx[i];
int idx = -1;
for (int j = 0; j < n_classes; j++) {
- if (are_key_placeholders_identical(classes[j].repr, k)) {
+ if (are_key_placeholders_identical(kx[class_repr[j]], kx[i])) {
idx = j;
break;
}
}
if (idx < 0) {
- if (n_classes >= CT_MAX_KEYEXPRS) {
- *out_canonical = false;
- return UINT64_MAX;
- }
- classes[n_classes].repr = k;
- classes[n_classes].n_pairs = 0;
+ // there can't be more classes than key expressions, hence no bound check is needed
+ class_repr[n_classes] = (uint8_t) i;
idx = n_classes++;
}
- if (classes[idx].n_pairs >= CT_MAX_KEYEXPRS) {
- *out_canonical = false;
- return UINT64_MAX;
- }
- classes[idx].pairs[classes[idx].n_pairs][0] = k->num_first;
- classes[idx].pairs[classes[idx].n_pairs][1] = k->num_second;
- classes[idx].n_pairs++;
+ class_of[i] = (uint8_t) idx;
}
// Canonical check: group by full key identity (musig groups stay whole) and
// require each group's sorted derivation pairs to be (0,1),(2,3),(4,5),...
+ // The pairs are collected one class at a time, since all the classes together have exactly n
+ // pairs in total.
+ uint32_t pairs[CT_MAX_KEYEXPRS][2];
*out_canonical = true;
- for (int i = 0; i < n_classes; i++) {
- sort_pairs(classes[i].pairs, classes[i].n_pairs);
- for (uint8_t j = 0; j < classes[i].n_pairs; j++) {
- if (classes[i].pairs[j][0] != (uint32_t) (2 * j) ||
- classes[i].pairs[j][1] != (uint32_t) (2 * j + 1)) {
+ for (int c = 0; c < n_classes; c++) {
+ uint8_t n_pairs = 0;
+ for (int i = 0; i < n; i++) {
+ if (class_of[i] != c) continue;
+ pairs[n_pairs][0] = kx[i]->num_first;
+ pairs[n_pairs][1] = kx[i]->num_second;
+ ++n_pairs;
+ }
+
+ sort_pairs(pairs, n_pairs);
+ for (uint8_t j = 0; j < n_pairs; j++) {
+ if (pairs[j][0] != (uint32_t) (2 * j) || pairs[j][1] != (uint32_t) (2 * j + 1)) {
*out_canonical = false;
}
}
}
// Compute an upper bound on the possible number of orderings for the
- // derivation pairs.
- uint32_t idx_vals[CT_MAX_KEYEXPRS * MAX_PUBKEYS_PER_MUSIG];
- uint32_t idx_cnts[CT_MAX_KEYEXPRS * MAX_PUBKEYS_PER_MUSIG];
+ // derivation pairs. Each of the at most CT_MAX_KEYEXPRS key expressions contributes at most
+ // MAX_PUBKEYS_PER_MUSIG plain keys, therefore each count fits in a uint8_t.
+ uint16_t idx_vals[CT_MAX_KEYEXPRS * MAX_PUBKEYS_PER_MUSIG];
+ uint8_t idx_cnts[CT_MAX_KEYEXPRS * MAX_PUBKEYS_PER_MUSIG];
int n_idx = 0;
for (int i = 0; i < n; i++) {
const policy_node_keyexpr_t *k = kx[i];
// Build the list of plain key indices contributed by this keyexpr.
- uint32_t members[MAX_PUBKEYS_PER_MUSIG];
+ uint16_t members[MAX_PUBKEYS_PER_MUSIG];
uint16_t n_members;
if (k->type == KEY_EXPRESSION_NORMAL) {
members[0] = k->k.key_index;
### src/common/wallet.c
@@ -23,12 +23,6 @@ typedef struct {
const char *name;
} token_descriptor_t;
-// As parse_script is recursive, we set a maximum reasonable recursion depth in order to avoid the
-// risk of stack exhaustion.
-// At the time of writing, the maximum depth measured across all the tests is 10, so 16 still
-// leaves a margin for much more complex scripts and seems unlikely to be hit in practice.
-#define MAX_PARSE_SCRIPT_RECURSION_DEPTH 16
-
static const token_descriptor_t KNOWN_TOKENS[] = {
{.type = TOKEN_SH, .name = "sh"},
{.type = TOKEN_WSH, .name = "wsh"},
@@ -604,6 +598,13 @@ static int parse_keyexpr(buffer_t *in_buf,
#define CONTEXT_WITHIN_WSH 2 // parsing a direct child of WSH
#define CONTEXT_WITHIN_TR 4 // parsing a child of TR (direct or not)
+// The remaining bits of the context flags count the THRESH nodes that contain the script being
+// parsed. Each of them costs about 600 bytes of stack while the extended info of the policy is
+// computed (see compute_thresh_ops), therefore their nesting is limited by MAX_THRESH_NESTING;
+// policies with thresh expressions with more nesting seem unlikely to be used in practice.
+#define CONTEXT_THRESH_NESTING_UNIT 8
+#define CONTEXT_THRESH_NESTING(flags) ((flags) / CONTEXT_THRESH_NESTING_UNIT)
+
// forward declaration
static int parse_script(buffer_t *in_buf,
buffer_t *out_buf,
@@ -688,6 +689,15 @@ static int parse_script(buffer_t *in_buf,
}
if (can_read && c == ':') {
+ // The wrappers are parsed in this same stack frame, but each of them creates a node
+ // containing the following one; therefore, they must be charged to the recursion
+ // budget explicitly. Otherwise, a short chain of wrappers that type-checks for any
+ // length (for example "nnn...n:pk(@0/**)") would produce an arbitrarily deep policy,
+ // exhausting the stack in the functions that walk it recursively.
+ if (depth + (size_t) n_wrappers > MAX_PARSE_SCRIPT_RECURSION_DEPTH) {
+ return WITH_ERROR(-1, "Script is too deeply nested");
+ }
+
// parse wrappers
for (int i = 0; i < n_wrappers; i++) {
policy_node_with_script_t *node =
@@ -740,6 +750,9 @@ static int parse_script(buffer_t *in_buf,
inner_wrapper = node;
}
buffer_seek_cur(in_buf, 1); // skip ":"
+
+ // the wrapped script is nested n_wrappers levels below the current one
+ depth += n_wrappers;
} else {
n_wrappers = 0; // it was not a wrapper
}
@@ -1370,6 +1383,12 @@ static int parse_script(buffer_t *in_buf,
break;
}
case TOKEN_THRESH: {
+ if (CONTEXT_THRESH_NESTING(context_flags) >= MAX_THRESH_NESTING) {
+ return WITH_ERROR(-1, "Too many nested thresh expressions");
+ }
+ // the children of this node (and all their descendants) are within one more thresh
+ unsigned int inner_context_flags = context_flags + CONTEXT_THRESH_NESTING_UNIT;
+
policy_node_thresh_t *node =
(policy_node_thresh_t *) buffer_alloc(out_buf, sizeof(policy_node_thresh_t), true);
if (node == NULL) {
@@ -1415,7 +1434,7 @@ static int parse_script(buffer_t *in_buf,
// parse a script into cur->script
buffer_alloc(out_buf, 0, true); // ensure alignment of current pointer
i_policy_node(&cur->script, buffer_get_cur(out_buf));
- if (0 > parse_script(in_buf, out_buf, version, depth + 1, context_flags)) {
+ if (0 > parse_script(in_buf, out_buf, version, depth + 1, inner_context_flags)) {
// failed while parsing internal script
return -1;
}
@@ -2108,12 +2127,14 @@ static int16_t maxcheck(int16_t a, int16_t b) {
return a > b ? a : b;
}
-// Maximum supported value for n in a thresh miniscript operator (technical limitation)
-#define MAX_N_IN_THRESH 128
-
-static int compute_thresh_ops(const policy_node_thresh_t *node,
- miniscript_ops_t *out,
- MiniscriptContext ctx) {
+// The two functions below are kept out of line on purpose: their arrays would otherwise be part of
+// the stack frame of compute_miniscript_policy_ext_info(), which is recursive, and would therefore
+// be reserved once per level of the policy even for the nodes that are not thresh. As they are,
+// they only use stack while a thresh node is being processed, and the nesting of thresh nodes is
+// limited to MAX_THRESH_NESTING while parsing.
+__attribute__((noinline)) static int compute_thresh_ops(const policy_node_thresh_t *node,
+ miniscript_ops_t *out,
+ MiniscriptContext ctx) {
uint16_t sats[MAX_N_IN_THRESH + 1 + 1] = {0};
uint16_t next_sats[MAX_N_IN_THRESH + 1 + 1] = {0}; // it temporarily uses an extra element
@@ -2150,9 +2171,9 @@ static int compute_thresh_ops(const policy_node_thresh_t *node,
return 0;
}
-static int compute_thresh_stacksize(const policy_node_thresh_t *node,
- miniscript_stacksize_t *out,
- MiniscriptContext ctx) {
+__attribute__((noinline)) static int compute_thresh_stacksize(const policy_node_thresh_t *node,
+ miniscript_stacksize_t *out,
+ MiniscriptContext ctx) {
uint16_t sats[MAX_N_IN_THRESH + 1 + 1] = {0};
uint16_t next_sats[MAX_N_IN_THRESH + 1 + 1] = {0}; // it temporarily uses an extra element
### src/common/wallet.h
@@ -57,6 +57,24 @@
#define MAX_DESCRIPTOR_TEMPLATE_LENGTH \
MAX(MAX_DESCRIPTOR_TEMPLATE_LENGTH_V1, MAX_DESCRIPTOR_TEMPLATE_LENGTH_V2)
+// As parse_script is recursive, we set a maximum reasonable recursion depth in order to avoid the
+// risk of stack exhaustion.
+// This depth is unlikely to be hit in practice.
+// Miniscript wrappers are counted as well: while they are not parsed recursively, they still
+// increase the depth of the parsed policy, which affects other recursive walkers.
+#define MAX_PARSE_SCRIPT_RECURSION_DEPTH 16
+
+// Maximum supported nesting of thresh operators
+#define MAX_THRESH_NESTING 4
+
+// Maximum supported value for n in a thresh miniscript operator (technical limitation).
+// It also bounds the stack used while analyzing a policy: the arrays of compute_thresh_ops() and
+// compute_thresh_stacksize() are proportional to it, and up to MAX_THRESH_NESTING of them are
+// alive at the same time due to recursion, therefore this ends up eating a substantial amount of
+// memory.
+// This limit is extremely unlikely to be hit in practice.
+#define MAX_N_IN_THRESH 24
+
// at most 92 bytes
// wallet type (1 byte)
// name length (1 byte)
### src/handler/lib/policy.c
@@ -18,8 +18,6 @@
#include "segwit_addr.h"
#include "wallet.h"
-#define MAX_POLICY_DEPTH 10
-
// The last opcode must be processed as a VERIFY flag
#define PROCESSOR_FLAG_V 1
@@ -46,7 +44,8 @@ typedef struct {
const wallet_derivation_info_t *wdi;
bool is_taproot;
- policy_parser_node_state_t nodes[MAX_POLICY_DEPTH]; // stack of nodes being processed
+ policy_parser_node_state_t
+ nodes[MAX_PARSE_SCRIPT_RECURSION_DEPTH]; // stack of nodes being processed
int node_stack_eos; // index of node being processed within nodes; will be set -1 at the end of
// processing
@@ -374,7 +373,7 @@ __attribute__((warn_unused_result)) static int state_stack_push(policy_parser_st
uint8_t flags) {
++state->node_stack_eos;
- if (state->node_stack_eos >= MAX_POLICY_DEPTH) {
+ if (state->node_stack_eos >= MAX_PARSE_SCRIPT_RECURSION_DEPTH) {
return WITH_ERROR(-1, "Reached maximum policy depth");
}
### src/handler/register_wallet.c
@@ -52,100 +52,36 @@ static const uint8_t BIP0341_NUMS_PUBKEY[] = {0x02, 0x50, 0x92, 0x9b, 0x74, 0xc1
0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0};
/**
- * Validates the input, initializes the hash context and starts accumulating the wallet header in
- * it.
+ * Fetches and validates the keys of the wallet policy, asks the user to confirm the registration,
+ * and sends the response.
+ *
+ * This is a separate (and explicitly not inlined) function because of its large buffers: they
+ * would otherwise be part of the stack frame of handler_register_wallet() while the descriptor
+ * template is parsed and validated, and those steps recurse over the parsed policy.
*/
-void handler_register_wallet(dispatcher_context_t *dc, uint8_t protocol_version) {
- UNUSED(protocol_version);
-
- LOG_PROCESSOR(__FILE__, __LINE__, __func__);
-
- policy_map_wallet_header_t wallet_header;
-
- uint8_t wallet_id[32];
- union {
- uint8_t bytes[MAX_WALLET_POLICY_BYTES];
- policy_node_t parsed;
- } policy_map;
-
+__attribute__((noinline)) static void confirm_and_register_wallet(
+ dispatcher_context_t *dc,
+ const policy_map_wallet_header_t *wallet_header,
+ const uint8_t *policy_map_descriptor,
+ const policy_node_t *policy,
+ const uint8_t wallet_id[static 32]) {
size_t n_internal_keys = 0;
- uint64_t serialized_policy_map_len;
- if (!buffer_read_varint(&dc->read_buffer, &serialized_policy_map_len)) {
- SEND_SW(dc, SW_WRONG_DATA_LENGTH);
- return;
- }
-
- uint8_t policy_map_descriptor[MAX_DESCRIPTOR_TEMPLATE_LENGTH + 1];
- if (0 > read_and_parse_wallet_policy(dc,
- &dc->read_buffer,
- &wallet_header,
- policy_map_descriptor,
- policy_map.bytes,
- sizeof(policy_map.bytes))) {
- SEND_SW(dc, SW_INCORRECT_DATA);
- return;
- }
- policy_map_descriptor[wallet_header.descriptor_template_len] = '\0';
-
- if (wallet_header.n_keys > MAX_N_KEYS_IN_WALLET_POLICY) {
- PRINTF("At most %d key expressions are supported in a wallet policy.\n",
- MAX_N_KEYS_IN_WALLET_POLICY);
- SEND_SW(dc, SW_NOT_SUPPORTED);
- return;
- }
-
- if (count_distinct_keys_info(&policy_map.parsed) != (int) wallet_header.n_keys) {
- PRINTF("The number of keys in descriptor template doesn't match the provided keys\n");
- SEND_SW(dc, SW_INCORRECT_DATA);
- return;
- }
-
- // Compute the wallet id (sha256 of the serialization)
- get_policy_wallet_id(&wallet_header, wallet_id);
-
- // Verify that the name is acceptable
- if (!is_policy_name_acceptable(wallet_header.name, wallet_header.name_len)) {
- PRINTF("Policy name is not acceptable\n");
- SEND_SW_EC(dc, SW_INCORRECT_DATA, EC_REGISTER_WALLET_UNACCEPTABLE_POLICY_NAME);
- return;
- }
-
- // check if policy is acceptable
- if (!is_policy_acceptable(&policy_map.parsed)) {
- PRINTF("Policy is not acceptable\n");
-
- SEND_SW(dc, SW_NOT_SUPPORTED);
- return;
- }
-
- // make sure that the policy is sane (especially if it contains miniscript)
- if (0 > is_policy_sane(dc,
- &policy_map.parsed,
- wallet_header.version,
- wallet_header.keys_info_merkle_root,
- wallet_header.n_keys)) {
- PRINTF("Policy is not sane\n");
-
- SEND_SW_EC(dc, SW_NOT_SUPPORTED, EC_REGISTER_WALLET_POLICY_NOT_SANE);
- return;
- }
-
uint32_t master_key_fingerprint = crypto_get_master_key_fingerprint();
char keys_info[MAX_N_KEYS_IN_WALLET_POLICY][MAX_POLICY_KEY_INFO_LEN + 1];
key_type_e keys_type[MAX_N_KEYS_IN_WALLET_POLICY];
memset(keys_type, 0, sizeof(keys_type));
- for (size_t cosigner_index = 0; cosigner_index < wallet_header.n_keys; cosigner_index++) {
+ for (size_t cosigner_index = 0; cosigner_index < wallet_header->n_keys; cosigner_index++) {
/**
* Receives and parses the next pubkey info.
* Asks the user to validate the pubkey info.
*/
int key_info_len = call_get_merkle_leaf_element(dc,
- wallet_header.keys_info_merkle_root,
- wallet_header.n_keys,
+ wallet_header->keys_info_merkle_root,
+ wallet_header->n_keys,
cosigner_index,
(uint8_t *) keys_info[cosigner_index],
MAX_POLICY_KEY_INFO_LEN);
@@ -161,7 +97,7 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t protocol_version)
buffer_t key_info_buffer = buffer_create(keys_info[cosigner_index], key_info_len);
policy_map_key_info_t key_info;
- if (parse_policy_map_key_info(&key_info_buffer, &key_info, wallet_header.version) == -1) {
+ if (parse_policy_map_key_info(&key_info_buffer, &key_info, wallet_header->version) == -1) {
PRINTF("Incorrect policy map.\n");
SEND_SW(dc, SW_INCORRECT_DATA);
return;
@@ -217,7 +153,7 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t protocol_version)
PRINTF("Wallet policy with no internal keys\n");
SEND_SW_EC(dc, SW_INCORRECT_DATA, EC_REGISTER_WALLET_POLICY_HAS_NO_INTERNAL_KEY);
return;
- } else if (n_internal_keys != 1 && wallet_header.version == WALLET_POLICY_VERSION_V1) {
+ } else if (n_internal_keys != 1 && wallet_header->version == WALLET_POLICY_VERSION_V1) {
// for legacy policies, we keep the restriction to exactly 1 internal key
PRINTF("V1 policies must have exactly 1 internal key\n");
SEND_SW(dc, SW_INCORRECT_DATA);
@@ -233,8 +169,8 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t protocol_version)
bool has_cleartext = false;
descriptor_class_e cleartext_class = DC_OTHER;
- if (cleartext_confusion_score(&policy_map.parsed) <= CLEARTEXT_MAX_CONFUSION_SCORE) {
- int rc = cleartext_encode(&policy_map.parsed,
+ if (cleartext_confusion_score(policy) <= CLEARTEXT_MAX_CONFUSION_SCORE) {
+ int rc = cleartext_encode(policy,
NULL,
cleartext_lines,
&n_cleartext_lines,
@@ -253,14 +189,14 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t protocol_version)
// the cleartext "Any K of <keys> must sign" captures the spending policy
// with little risk of ambiguity. Therefore, it is safe to hide the
// raw descriptor template, simplifying the UX.
- const char *descriptor_to_show = (char *) policy_map_descriptor;
+ const char *descriptor_to_show = (const char *) policy_map_descriptor;
if (has_cleartext && n_cleartext_lines > 0 && cleartext_class == DC_MULTISIG) {
descriptor_to_show = NULL;
}
// show wallet policy
if (!ui_display_register_wallet_policy(dc,
- &wallet_header,
+ wallet_header,
descriptor_to_show,
&cleartext_lines,
n_cleartext_lines,
@@ -275,7 +211,7 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t protocol_version)
uint8_t hmac[32];
} response;
- memcpy(response.wallet_id, wallet_id, sizeof(wallet_id));
+ memcpy(response.wallet_id, wallet_id, 32);
if (!compute_wallet_hmac(wallet_id, response.hmac)) {
SEND_SW(dc, SW_BAD_STATE); // this should never fail
@@ -285,6 +221,91 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t protocol_version)
SEND_RESPONSE(dc, &response, sizeof(response), SW_OK);
}
+/**
+ * Validates the input, initializes the hash context and starts accumulating the wallet header in
+ * it.
+ */
+void handler_register_wallet(dispatcher_context_t *dc, uint8_t protocol_version) {
+ UNUSED(protocol_version);
+
+ LOG_PROCESSOR(__FILE__, __LINE__, __func__);
+
+ policy_map_wallet_header_t wallet_header;
+
+ uint8_t wallet_id[32];
+ union {
+ uint8_t bytes[MAX_WALLET_POLICY_BYTES];
+ policy_node_t parsed;
+ } policy_map;
+
+ uint64_t serialized_policy_map_len;
+ if (!buffer_read_varint(&dc->read_buffer, &serialized_policy_map_len)) {
+ SEND_SW(dc, SW_WRONG_DATA_LENGTH);
+ return;
+ }
+
+ uint8_t policy_map_descriptor[MAX_DESCRIPTOR_TEMPLATE_LENGTH + 1];
+ if (0 > read_and_parse_wallet_policy(dc,
+ &dc->read_buffer,
+ &wallet_header,
+ policy_map_descriptor,
+ policy_map.bytes,
+ sizeof(policy_map.bytes))) {
+ SEND_SW(dc, SW_INCORRECT_DATA);
+ return;
+ }
+ policy_map_descriptor[wallet_header.descriptor_template_len] = '\0';
+
+ if (wallet_header.n_keys > MAX_N_KEYS_IN_WALLET_POLICY) {
+ PRINTF("At most %d key expressions are supported in a wallet policy.\n",
+ MAX_N_KEYS_IN_WALLET_POLICY);
+ SEND_SW(dc, SW_NOT_SUPPORTED);
+ return;
+ }
+
+ if (count_distinct_keys_info(&policy_map.parsed) != (int) wallet_header.n_keys) {
+ PRINTF("The number of keys in descriptor template doesn't match the provided keys\n");
+ SEND_SW(dc, SW_INCORRECT_DATA);
+ return;
+ }
+
+ // Compute the wallet id (sha256 of the serialization)
+ get_policy_wallet_id(&wallet_header, wallet_id);
+
+ // Verify that the name is acceptable
+ if (!is_policy_name_acceptable(wallet_header.name, wallet_header.name_len)) {
+ PRINTF("Policy name is not acceptable\n");
+ SEND_SW_EC(dc, SW_INCORRECT_DATA, EC_REGISTER_WALLET_UNACCEPTABLE_POLICY_NAME);
+ return;
+ }
+
+ // check if policy is acceptable
+ if (!is_policy_acceptable(&policy_map.parsed)) {
+ PRINTF("Policy is not acceptable\n");
+
+ SEND_SW(dc, SW_NOT_SUPPORTED);
+ return;
+ }
+
+ // make sure that the policy is sane (especially if it contains miniscript)
+ if (0 > is_policy_sane(dc,
+ &policy_map.parsed,
+ wallet_header.version,
+ wallet_header.keys_info_merkle_root,
+ wallet_header.n_keys)) {
+ PRINTF("Policy is not sane\n");
+
+ SEND_SW_EC(dc, SW_NOT_SUPPORTED, EC_REGISTER_WALLET_POLICY_NOT_SANE);
+ return;
+ }
+
+ confirm_and_register_wallet(dc,
+ &wallet_header,
+ policy_map_descriptor,
+ &policy_map.parsed,
+ wallet_id);
+}
+
static bool is_policy_acceptable(const policy_node_t *policy) {
return policy->type == TOKEN_PKH || policy->type == TOKEN_WPKH || policy->type == TOKEN_SH ||
policy->type == TOKEN_WSH || policy->type == TOKEN_TR;
### tests/test_register_wallet.py
@@ -560,3 +560,54 @@ def test_register_wallet_too_many_derivation_steps(navigator: Navigator, firmwar
client.register_wallet(wallet)
assert DeviceException.exc.get(e.value.status) == IncorrectDataError
+
+
+# an app that does not bound the policy depth accepts the policy below and waits for a confirmation
+# that never comes, as no navigator is passed; the timeout turns that wait into a failure
+@pytest.mark.timeout(60)
+def test_register_wallet_deep_wrapper_chain(client: RaggerClient):
+ """A policy far deeper than its template must be refused, without crashing the app."""
+ # "n:" maps B -> B, so the chain type-checks whatever its length, and each wrapper adds an AST
+ # level for one template byte. 59 of them describe a 60-level policy in 74 bytes, and still fit
+ # the app's 896-byte policy buffer, so the deep walk is really reached.
+ wallet = WalletPolicy(
+ name="Wrapper chain",
+ descriptor_template="wsh(" + "n" * 59 + ":pk(@0/**))",
+ keys_info=["[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK"],
+ )
+
+ with pytest.raises(ExceptionRAPDU) as e:
+ client.register_wallet(wallet)
+
+ assert DeviceException.exc.get(e.value.status) == IncorrectDataError
+ assert len(e.value.data) == 0
+
+ # a crash would have closed the connection
+ assert len(client.get_master_fingerprint()) == 4
+
+
+def test_register_wallet_max_depth(navigator: Navigator, firmware: Firmware, client:
+ RaggerClient):
+ """Registers the deepest policies that the app accepts, which must succeed."""
+
+ # limits copied from the app; keep in sync
+ MAX_DEPTH = 16 # MAX_PARSE_SCRIPT_RECURSION_DEPTH, src/common/wallet.h
+ MAX_THRESH_NESTING = 4 # src/common/wallet.h
+
+ policies = [
+ # wsh() takes one level, leaving MAX_DEPTH - 1 for the wrappers
+ "wsh(" + "n" * (MAX_DEPTH - 1) + ":pk(@0/**))",
+ ("wsh(" + "thresh(1," * MAX_THRESH_NESTING + "pk(@0/**)"
+ + ")" * MAX_THRESH_NESTING + ")"),
+ ]
+
+ for descriptor_template in policies:
+ wallet = WalletPolicy(name="Deep policy",
+ descriptor_template=descriptor_template,
+ keys_info=["[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK"])
+
+ wallet_id, _ = client.register_wallet(
+ wallet,
+ navigator,
+ instructions=register_wallet_instruction_approve_no_save(firmware))
+ assert wallet_id == wallet.id
### unit-tests/test_wallet.c
@@ -899,6 +899,129 @@ static void test_traverse_callback_abort(void **state) {
assert_int_equal(s.types[1], TOKEN_OR_I);
}
+/* ------------------------------------------------------------------------
+ * Maximum policy depth
+ *
+ * The parser bounds the depth of the parsed policy, as several functions walk
+ * it recursively. Miniscript wrappers create a node each, exactly like nested
+ * script expressions, and are therefore charged to the same budget: without
+ * that, a template as short as "wsh(nnn...n:pk(@0/**))" produces a policy
+ * hundreds of levels deep, and the recursive walkers (in particular
+ * compute_miniscript_policy_ext_info, used to check that a policy is sane) run
+ * out of stack while processing it.
+ * ------------------------------------------------------------------------ */
+
+// Builds "wsh(" + n_wrappers copies of "n" + ":pk(@0/**))" into out.
+// The wrapped script is at depth 1 + n_wrappers.
+static void make_wrapper_chain(char *out, size_t out_size, int n_wrappers) {
+ assert_true((size_t) n_wrappers + sizeof("wsh(:pk(@0/**))") <= out_size);
+ char *p = out + sprintf(out, "wsh(");
+ for (int i = 0; i < n_wrappers; i++) *p++ = 'n';
+ strcpy(p, ":pk(@0/**))");
+}
+
+static void test_parse_policy_max_depth_wrappers(void **state) {
+ (void) state;
+
+ // deep policies need more memory than the simple ones of the other tests
+ uint8_t out[4 * MAX_WALLET_POLICY_MEMORY_SIZE];
+ char policy[MAX_DESCRIPTOR_TEMPLATE_LENGTH + 1];
+
+ // the script inside wsh() is at depth 1, therefore one wrapper less than the limit fits
+ make_wrapper_chain(policy, sizeof(policy), MAX_PARSE_SCRIPT_RECURSION_DEPTH - 1);
+ assert_true(0 <= parse_policy(policy, out, sizeof(out)));
+
+ // ...and the chain is rejected as soon as it exceeds the budget, even by one
+ make_wrapper_chain(policy, sizeof(policy), MAX_PARSE_SCRIPT_RECURSION_DEPTH);
+ assert_true(0 > parse_policy(policy, out, sizeof(out)));
+
+ // a much longer chain still fits in a descriptor template; it must be rejected while parsing,
+ // before any recursive walk of the parsed policy
+ make_wrapper_chain(policy, sizeof(policy), 200);
+ assert_true(0 > parse_policy(policy, out, sizeof(out)));
+
+ // the deepest policy that is accepted must be processed correctly (and within the available
+ // stack) by the recursive functions that walk it afterwards
+ const int n_wrappers = MAX_PARSE_SCRIPT_RECURSION_DEPTH - 1;
+ make_wrapper_chain(policy, sizeof(policy), n_wrappers);
+ assert_true(0 <= parse_policy(policy, out, sizeof(out)));
+
+ const policy_node_t *inner = r_policy_node(&((policy_node_with_script_t *) out)->script);
+ policy_node_ext_info_t ext_info;
+ assert_int_equal(compute_miniscript_policy_ext_info(inner, &ext_info, MINISCRIPT_CONTEXT_P2WSH),
+ 0);
+ // n:X adds a single OP_0NOTEQUAL on top of the 1 opcode of pk(key)
+ assert_int_equal(ext_info.ops.count, n_wrappers + 1);
+}
+
+// Builds "wsh(" + n_levels copies of "thresh(1," + "pk(@0/**)" + the closing parentheses.
+static void make_thresh_chain(char *out, size_t out_size, int n_levels) {
+ assert_true((size_t) n_levels * 10 + sizeof("wsh(pk(@0/**))") <= out_size);
+ char *p = out + sprintf(out, "wsh(");
+ for (int i = 0; i < n_levels; i++) p += sprintf(p, "thresh(1,");
+ p += sprintf(p, "pk(@0/**)");
+ for (int i = 0; i < n_levels; i++) *p++ = ')';
+ strcpy(p, ")");
+}
+
+static void test_parse_policy_max_thresh_nesting(void **state) {
+ (void) state;
+
+ uint8_t out[4 * MAX_WALLET_POLICY_MEMORY_SIZE];
+ char policy[MAX_DESCRIPTOR_TEMPLATE_LENGTH + 1];
+
+ make_thresh_chain(policy, sizeof(policy), MAX_THRESH_NESTING);
+ assert_true(0 <= parse_policy(policy, out, sizeof(out)));
+
+ // the deepest accepted nesting must also be processed by the recursive walkers
+ const policy_node_t *inner = r_policy_node(&((policy_node_with_script_t *) out)->script);
+ policy_node_ext_info_t ext_info;
+ assert_int_equal(compute_miniscript_policy_ext_info(inner, &ext_info, MINISCRIPT_CONTEXT_P2WSH),
+ 0);
+
+ make_thresh_chain(policy, sizeof(policy), MAX_THRESH_NESTING + 1);
+ assert_true(0 > parse_policy(policy, out, sizeof(out)));
+
+ // a chain of nested thresh still fits in a descriptor template well beyond the depth limit
+ make_thresh_chain(policy, sizeof(policy), MAX_PARSE_SCRIPT_RECURSION_DEPTH + 1);
+ assert_true(0 > parse_policy(policy, out, sizeof(out)));
+
+ // the limit is on nested thresh only: many thresh nodes as siblings are still accepted
+ assert_true(0 <=
+ parse_policy("wsh(thresh(1,thresh(1,pk(@0/**)),sc:pk_k(@1/**),sc:pk_k(@2/**)))",
+ out,
+ sizeof(out)));
+}
+
+// Builds "wsh(thresh(1,pk(@0/**)" + n_branches - 1 copies of ",a:0" + "))".
+static void make_wide_thresh(char *out, size_t out_size, int n_branches) {
+ assert_true((size_t) n_branches * 4 + sizeof("wsh(thresh(1,pk(@0/**)))") <= out_size);
+ char *p = out + sprintf(out, "wsh(thresh(1,pk(@0/**)");
+ for (int i = 1; i < n_branches; i++) p += sprintf(p, ",a:0");
+ strcpy(p, "))");
+}
+
+// A thresh with more branches than the analysis supports must be reported as an error, rather
+// than being analyzed with a truncated (or overflowing) dynamic programming table.
+static void test_max_n_in_thresh(void **state) {
+ (void) state;
+
+ uint8_t out[4 * MAX_WALLET_POLICY_MEMORY_SIZE];
+ char policy[MAX_DESCRIPTOR_TEMPLATE_LENGTH + 1];
+ policy_node_ext_info_t ext_info;
+
+ make_wide_thresh(policy, sizeof(policy), MAX_N_IN_THRESH);
+ assert_true(0 <= parse_policy(policy, out, sizeof(out)));
+ const policy_node_t *inner = r_policy_node(&((policy_node_with_script_t *) out)->script);
+ assert_int_equal(compute_miniscript_policy_ext_info(inner, &ext_info, MINISCRIPT_CONTEXT_P2WSH),
+ 0);
+
+ make_wide_thresh(policy, sizeof(policy), MAX_N_IN_THRESH + 1);
+ assert_true(0 <= parse_policy(policy, out, sizeof(out)));
+ inner = r_policy_node(&((policy_node_with_script_t *) out)->script);
+ assert_true(0 > compute_miniscript_policy_ext_info(inner, &ext_info, MINISCRIPT_CONTEXT_P2WSH));
+}
+
int main() {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_parse_policy_map_singlesig_1),
@@ -925,6 +1048,9 @@ int main() {
cmocka_unit_test(test_traverse_tr_two_leaves),
cmocka_unit_test(test_traverse_tr_nested_tree),
cmocka_unit_test(test_traverse_callback_abort),
+ cmocka_unit_test(test_parse_policy_max_depth_wrappers),
+ cmocka_unit_test(test_parse_policy_max_thresh_nesting),
+ cmocka_unit_test(test_max_n_in_thresh),
};
return cmocka_run_group_tests(tests, NULL, NULL);Why this scored 72/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.