hsmd: fix HSM sent an unknown message type error
What changed, and why it matters
This commit fixes a bug in Core Lightning's Hardware Security Module daemon (hsmd). When a user entered the wrong wallet passphrase, the daemon would crash with a confusing 'HSM sent unknown message type' error and print a stack trace. The fix ensures the error message is sent using the correct wire protocol format and that wrong-passphrase failures exit cleanly without a stack trace. It is primarily a reliability and user-experience fix, not a security vulnerability that allows remote exploitation.
Apply the patch. It is a low-risk correctness fix that prevents a confusing crash and stack trace leak on wrong passphrase. No immediate incident response is required because the condition requires local wrong-passphrase input and does not grant unauthorized access.
Security signals we found
Daemon crash on malformed inter-process message
Missing wire-protocol length prefix in IPC message
Stack trace disclosure on user error (wrong passphrase)
Regression test added for graceful failure handling
Evidence from the diff
The patch changes hsmd’s failure path to use wire_sync_write() instead of write_all() when sending an init-reply-failure message to lightningd. The raw write_all() omitted the wire protocol length prefix, causing lightningd to misparse the message as an unknown type. Additionally, a wrong-passphrase error is now treated as a user error: hsmd sends a formatted failure message and exits cleanly (exit 0) rather than calling status_failed() and dumping a backtrace. A regression test is added in tests/test_wallet.py.
Changed components
hsmd/hsmd.ctests/test_wallet.pyInspect captured patch +55 / −1
diff --git a/hsmd/hsmd.c b/hsmd/hsmd.c
index 8391164..dfe61fc 100644
--- a/hsmd/hsmd.c
+++ b/hsmd/hsmd.c
@@ -33,6 +33,7 @@
#include <wally_bip39.h>
#include <wally_core.h>
#include <wire/wire_io.h>
+#include <wire/wire_sync.h>
/*~ Each subdaemon is started with stdin connected to lightningd (for status
* messages), and stderr untouched (for emergency printing). File descriptors
@@ -291,7 +292,7 @@ static void hsmd_send_init_reply_failure(enum hsm_secret_error error_code, enum
msg = towire_hsmd_init_reply_failure(NULL, error_code, formatted_msg);
if (msg) {
/* Send directly to lightningd via REQ_FD */
- write_all(REQ_FD, msg, tal_bytelen(msg));
+ wire_sync_write(REQ_FD, msg);
tal_free(msg);
}
@@ -441,6 +442,15 @@ static void load_hsm(const char *passphrase)
tal_bytelen(hsm_secret_contents),
passphrase, &err);
if (!hsms) {
+ /* Wrong passphrase is user error, not internal error - exit cleanly */
+ if (err == HSM_SECRET_ERR_WRONG_PASSPHRASE) {
+ u8 *msg = towire_hsmd_init_reply_failure(NULL, err,
+ tal_fmt(tmpctx,
+ "Failed to load hsm_secret: %s",
+ hsm_secret_error_str(err)));
+ wire_sync_write(REQ_FD, msg);
+ exit(0);
+ }
hsmd_send_init_reply_failure(err, STATUS_FAIL_INTERNAL_ERROR,
"Failed to load hsm_secret: %s", hsm_secret_error_str(err));
}
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index 0dfa66c..e92d057 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -2492,3 +2492,47 @@ def test_pending_payments_cleanup(node_factory, bitcoind):
l1 = node_factory.get_node(dbfile='l1-pending-sendpays-with-no-htlc.sqlite3.xz', options={'database-upgrade': True})
assert [p['status'] for p in l1.rpc.listsendpays()['payments']] == ['failed', 'pending']
assert [p['status'] for p in l1.rpc.listpays()['pays']] == ['pending']
+
+
+@unittest.skipIf(VALGRIND, "It does not play well with prompt and key derivation.")
+def test_hsm_wrong_passphrase_crash(node_factory):
+ """Test that hsmd handles wrong passphrase gracefully without crashing.
+
+ This test reproduces a bug where hsmd would crash with "HSM sent unknown message type"
+ when a wrong passphrase was provided. The issue was that hsmd_send_init_reply_failure
+ was using write_all() instead of wire_sync_write(), missing the length prefix.
+ """
+ l1 = node_factory.get_node(start=False, expect_fail=True)
+ hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret")
+ os.remove(hsm_path)
+
+ # Create hsm_secret with a passphrase
+ passphrase = "correct_passphrase"
+ mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
+
+ hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path)
+ master_fd, slave_fd = os.openpty()
+ hsmtool.start(stdin=slave_fd)
+ hsmtool.wait_for_log(r"Introduce your BIP39 word list")
+ write_all(master_fd, f"{mnemonic}\n".encode("utf-8"))
+ hsmtool.wait_for_log(r"Enter your passphrase:")
+ write_all(master_fd, f"{passphrase}\n".encode("utf-8"))
+ assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0
+ os.close(master_fd)
+ os.close(slave_fd)
+
+ # Try to start with wrong passphrase
+ l1.daemon.opts["hsm-passphrase"] = None
+ master_fd2, slave_fd2 = os.openpty()
+ l1.daemon.start(stdin=slave_fd2, wait_for_initialized=False, stderr_redir=True)
+ l1.daemon.wait_for_log("Enter hsm_secret passphrase:")
+ write_all(master_fd2, "wrong_passphrase\n".encode("utf-8"))
+
+ # Should fail gracefully with proper error message, not "unknown message type"
+ l1.daemon.wait()
+ assert l1.daemon.is_in_stderr("Failed to load hsm_secret: Wrong passphrase")
+ assert not l1.daemon.is_in_stderr("HSM sent unknown message type")
+ assert not l1.daemon.is_in_stderr("send_backtrace") # No backtrace for user error
+
+ os.close(master_fd2)
+ os.close(slave_fd2)
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.