From 761732221a0a04bfa0683a02d8e0e63416963ba8 Mon Sep 17 00:00:00 2001 From: 0xfandom Date: Mon, 18 May 2026 15:15:57 +0530 Subject: [PATCH 1/2] cmd/commands: read full stdin for lncli unlock --stdin The --stdin branch of `lncli unlock` used bufio.ReadBytes('\n'), which stops at the first newline byte and silently truncates passwords that contain embedded newlines. A wallet password generated from random bytes can legitimately contain a newline, in which case the unlock attempt fails even though the same password works over REST/gRPC. Switch to io.ReadAll so the password is consumed up to EOF, and only trim a single trailing newline (with optional CR) so the common `echo "pw" | lncli unlock --stdin` invocation keeps working without leaking the trailing byte added by the shell. New table-driven test cases cover an embedded newline, no trailing newline, and a CRLF terminator. Fixes #5584 --- cmd/commands/cmd_walletunlocker.go | 15 +++-- cmd/commands/cmd_walletunlocker_test.go | 79 +++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/cmd/commands/cmd_walletunlocker.go b/cmd/commands/cmd_walletunlocker.go index 0c04ce25c0d..de4bcae9c4e 100644 --- a/cmd/commands/cmd_walletunlocker.go +++ b/cmd/commands/cmd_walletunlocker.go @@ -543,11 +543,16 @@ func unlockWithDeps(ctx *cli.Context, // password manager. If the user types the password instead, it will be // echoed in the console. case ctx.IsSet("stdin"): - reader := bufio.NewReader(stdin) - pw, err = reader.ReadBytes('\n') - - // Remove carriage return and newline characters. - pw = bytes.Trim(pw, "\r\n") + // Read until EOF so passwords containing newline bytes are + // preserved. A single trailing newline (with optional CR) is + // stripped so the common `echo "pw" | lncli unlock --stdin` + // usage keeps working. + pw, err = io.ReadAll(stdin) + if err != nil { + return err + } + pw = bytes.TrimSuffix(pw, []byte{'\n'}) + pw = bytes.TrimSuffix(pw, []byte{'\r'}) // Read the password from a terminal by default. This requires the // terminal to be a real tty and will fail if a string is piped into diff --git a/cmd/commands/cmd_walletunlocker_test.go b/cmd/commands/cmd_walletunlocker_test.go index 91e4be91368..63d00fea708 100644 --- a/cmd/commands/cmd_walletunlocker_test.go +++ b/cmd/commands/cmd_walletunlocker_test.go @@ -266,6 +266,85 @@ func TestUnlock(t *testing.T) { }, }, + // Password piped via stdin contains an embedded newline. + // Reading must consume everything up to EOF, preserving the + // embedded newline byte. + { + name: "success_stdin_embedded_newline", + args: []string{"--stdin"}, + stdinInput: "first\nsecond\n", + stateStreams: []stateStreamSpec{ + { + states: []lnrpc.WalletState{ + lnrpc.WalletState_LOCKED, + }, + }, + { + states: []lnrpc.WalletState{ + lnrpc.WalletState_RPC_ACTIVE, + }, + }, + }, + expectReadPasswordCalls: 0, + expectUnlockCalls: 1, + expectSubscribeCalls: 2, + expectReq: &lnrpc.UnlockWalletRequest{ + WalletPassword: []byte("first\nsecond"), + }, + }, + + // Password piped via stdin without any trailing newline (e.g. + // `printf %s pw | lncli unlock --stdin`). + { + name: "success_stdin_no_trailing_newline", + args: []string{"--stdin"}, + stdinInput: "secret", + stateStreams: []stateStreamSpec{ + { + states: []lnrpc.WalletState{ + lnrpc.WalletState_LOCKED, + }, + }, + { + states: []lnrpc.WalletState{ + lnrpc.WalletState_RPC_ACTIVE, + }, + }, + }, + expectReadPasswordCalls: 0, + expectUnlockCalls: 1, + expectSubscribeCalls: 2, + expectReq: &lnrpc.UnlockWalletRequest{ + WalletPassword: []byte("secret"), + }, + }, + + // Password piped via stdin with a CRLF terminator: only the + // final \r\n pair is stripped. + { + name: "success_stdin_crlf_terminator", + args: []string{"--stdin"}, + stdinInput: "secret\r\n", + stateStreams: []stateStreamSpec{ + { + states: []lnrpc.WalletState{ + lnrpc.WalletState_LOCKED, + }, + }, + { + states: []lnrpc.WalletState{ + lnrpc.WalletState_RPC_ACTIVE, + }, + }, + }, + expectReadPasswordCalls: 0, + expectUnlockCalls: 1, + expectSubscribeCalls: 2, + expectReq: &lnrpc.UnlockWalletRequest{ + WalletPassword: []byte("secret"), + }, + }, + // Uses positional recovery window argument. { name: "success_arg_recovery_window", From 5810babd0540ab40a2b1c3b6843e0afb11df967d Mon Sep 17 00:00:00 2001 From: 0xfandom Date: Mon, 18 May 2026 15:16:01 +0530 Subject: [PATCH 2/2] docs: add release note for lncli unlock --stdin EOF fix --- docs/release-notes/release-notes-0.22.0.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes/release-notes-0.22.0.md b/docs/release-notes/release-notes-0.22.0.md index fb31bad5662..2e7bb74f7e5 100644 --- a/docs/release-notes/release-notes-0.22.0.md +++ b/docs/release-notes/release-notes-0.22.0.md @@ -26,6 +26,10 @@ [clarifies](https://github.com/lightningnetwork/lnd/issues/10568) the ZMQ port-mismatch warnings so they no longer suggest that the connection failed. +* [`lncli unlock --stdin`](https://github.com/lightningnetwork/lnd/pull/10784) + now reads the password until EOF instead of stopping at the first newline, + so passwords containing embedded newline bytes are accepted. + # New Features ## Functional Enhancements