What changed, and why it matters
This commit adds safety checks to the Ledger Bitcoin app so that the 'older(n)' time-lock feature can only use values that actually have meaning under Bitcoin's consensus rules. Before this fix, a user could be shown a wallet policy with a value like older(65536), which looks like a real time-lock but behaves like no time-lock at all. That could trick a user into believing funds are locked when they are not. The patch rejects such misleading values.
Treat as a security-hardening commit. Review whether the same validation should be applied to other relative-timelock constructs (e.g., after() or similar absolute/relative locktime nodes) and ensure the Ledger app's UI does not display misleading locktime values. No immediate incident response is indicated, but the change should be included in release notes as a safety improvement.
Security signals we found
Input validation added for relative timelock argument
Comment explicitly describes dangerous equivalence (older(65536) behaves as older(0))
References upstream Bitcoin Core hardening PR #33135
Validation forces unused consensus bits to zero per BIP-68/BIP-112
Patch is defensive hardening rather than a full bug fix
Evidence from the diff
The change introduces a depth-first traversal callback (check_older_node_cb) in is_policy_sane() that validates every TOKEN_OLDER node. Per BIP-68/BIP-112, only the lower 16 bits and the SEQUENCE_LOCKTIME_TYPE_FLAG (bit 22) carry consensus meaning for nSequence-based relative timelocks. The callback masks off bit 22 and requires the remaining value to be between 1 and 65535 inclusive, which forces bits 16-21 and 23-31 to be zero. This prevents semantically equivalent but misleading encodings such as older(65536) (equivalent to older(0)) or older(4259840) (equivalent to older(4194304)). The commit message and comments explicitly describe this as a safety issue and reference Bitcoin Core PR #33135.
Changed components
src/handler/lib/policy.cis_policy_sane()Miniscript/older() policy node handlingInspect captured patch +34 / −1
diff --git a/src/common/wallet.h b/src/common/wallet.h
index 9180174..0daf9ab 100644
--- a/src/common/wallet.h
+++ b/src/common/wallet.h
@@ -509,7 +509,7 @@ int compute_miniscript_policy_ext_info(const policy_node_t *policy_node,
MiniscriptContext ctx);
/**
- * Callback type for traverse_policy_script_tree.
+ * Callback type for traverse_policy_dfs.
* Called for each node in depth-first (pre-order) traversal.
*
* @param node pointer to the current policy node
diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c
index 3134476..1e10826 100644
--- a/src/handler/lib/policy.c
+++ b/src/handler/lib/policy.c
@@ -1945,6 +1945,25 @@ static bool are_key_placeholders_identical(const policy_node_keyexpr_t *kp1,
LEDGER_ASSERT(false, "Unreachable code");
}
+/**
+ * Callback for traverse_policy_dfs that rejects any TOKEN_OLDER node whose argument is not either:
+ * - between 1 and 65535 (inclusive), if bit 22 is cleared (block-based relative timelock)
+ * - between 1 + 2^22 = 4194305 and 65535 + 2^22 = 4259839 (inclusive), if bit 22 is set (time-based
+ * relative timelock) This forces all the bits that have no consensus meaning per BIP-68/BIP-112 to
+ * be zero.
+ */
+static int check_older_node_cb(const policy_node_t *node, void *callback_state) {
+ (void) callback_state;
+ if (node->type == TOKEN_OLDER) {
+ const policy_node_with_uint32_t *older = (const policy_node_with_uint32_t *) node;
+ uint32_t n = older->n & ~SEQUENCE_LOCKTIME_TYPE_FLAG;
+ if (n < 1 || n > 65535) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
int is_policy_sane(dispatcher_context_t *dispatcher_context,
const policy_node_t *policy,
int wallet_version,
@@ -2069,6 +2088,20 @@ int is_policy_sane(dispatcher_context_t *dispatcher_context,
}
}
}
+
+ // For relative timelocks with `older(n)`, bits 16 to 21 and 23 to 31 have no consensus meaning
+ // per BIP-68/BIP-112. Therefore, for example, `older(65536)` is equivalent to `older(0)`, which
+ // could be dangerous as the user might expect that a timelock is enforced. For relative
+ // timelocks, we reject any value outside the following safe ranges:
+ // - between 1 and 65535 (inclusive), if bit 22 is cleared (block-based timelock)
+ // - between 1 + 2^22 = 4194305 and 65535 + 2^22 = 4259839 (inclusive), if bit 22 is set
+ // (time-based timelock)
+ //
+ // See also: https://github.com/bitcoin/bitcoin/pull/33135
+ if (0 > traverse_policy_dfs(policy, check_older_node_cb, NULL)) {
+ return WITH_ERROR(-1, "older() argument out of valid range");
+ }
+
return 0;
}
Why this scored 62/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.