{
 "number": 35833,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35833",
 "title": "log: prevent user input from injecting fake log lines",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-07-29T00:02:06Z",
 "updated_at": "2026-09-15T21:21:18Z",
 "age_days": 50,
 "draft": false,
 "labels": [
  "Utils/log/libs"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "54c71d72e842a7286b81db3275368a5f43a8a7df",
 "head_ref": "l0rinc/sanitize-log-inputs",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2026-07-29T06:21:53Z",
   "sha": "ed4eb51e9fc6f62975e272e96b22e0a6b64d3205"
  },
  {
   "t": "2026-09-15T07:34:12Z",
   "sha": "aae82ddcc52a2f87a73077d150f9dd95ef7e572d"
  },
  {
   "t": "2026-09-15T21:11:15Z",
   "sha": "54c71d72e842a7286b81db3275368a5f43a8a7df"
  }
 ],
 "additions": 75,
 "deletions": 18,
 "changed_files": 9,
 "commit_count": 3,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "stale_ack": [
     {
      "login": "achow101",
      "url": "https://github.com/bitcoin/bitcoin/pull/35833#issuecomment-5223311531"
     },
     {
      "login": "polespinasa",
      "url": "https://github.com/bitcoin/bitcoin/pull/35833#pullrequestreview-5208039434"
     },
     {
      "login": "ryanofsky",
      "url": "https://github.com/bitcoin/bitcoin/pull/35833#pullrequestreview-5210602751"
     }
    ]
   },
   "conflicts": [
    {
     "number": 35768,
     "title": "wallet: Reject whitespace-only wallet names",
     "author": "vicjuma"
    }
   ]
  }
 },
 "acks_parsed": {
  "achow101": {
   "kind": "ack",
   "hash": "ed4eb51e9fc6f62975e272e96b22e0a6b64d3205",
   "t": "2026-08-07T23:52:39Z",
   "stale": true
  },
  "ryanofsky": {
   "kind": "ack",
   "hash": "aae82ddcc52a2f87a73077d150f9dd95ef7e572d",
   "t": "2026-09-15T14:32:37Z",
   "stale": true
  },
  "polespinasa": {
   "kind": "ack",
   "hash": "aae82ddcc52a2f87a73077d150f9dd95ef7e572d",
   "t": "2026-09-15T09:19:00Z",
   "stale": true
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 3,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 1,
  "changes_requested": 0,
  "distinct_reviewers": [
   "Crypt-iQ",
   "achow101",
   "davidgumberg",
   "maflcko",
   "polespinasa",
   "ryanofsky"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-15T21:21:17Z",
  "last_reviewer_activity": "2026-09-15T14:40:19Z",
  "last_reviewer": "polespinasa",
  "author_silent_days": 1,
  "waiting_on_author_days": 0,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": [
   35768
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/httprpc.cpp",
  "src/util/exception.cpp",
  "src/util/string.h"
 ],
 "body": "**Problem:** Restricted RPC users and callers of `createwallet` or `restorewallet` can inject newlines through rejected methods or wallet names, and [global log escaping preserves them](https://github.com/bitcoin/bitcoin/pull/17095), making forged lines look like node messages.\n\n**Fix:** Escape embedded newlines in log messages and emit intentional multiline output as separate log entries.\nAuthorization still uses the original method string, and wallet naming and loading behavior remain unchanged.\n\nManual reproducer\n\n```bash\nDATADIR=\"$(mktemp -d /tmp/bitcoin-log-injection.XXXXXX)\"\nM=\"$(date -u +%Y-%m-%dT%H:%M:%SZ) ERROR: ConnectTip: ConnectBlock 0000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef failed, bad-txns-inputs-missingorspent\"\ncmake -B build >/dev/null 2>&1 && cmake --build build -j >/dev/null 2>&1\nbuild/bin/bitcoind -regtest -daemonwait -datadir=\"$DATADIR\" -rpcwhitelist=__cookie__:getblock,stop >/dev/null 2>&1\nbuild/bin/bitcoin-cli -regtest -datadir=\"$DATADIR\" $'getblock\\n'\"$M\" >/dev/null 2>&1; killall bitcoind >/dev/null\necho; grep -E 'ConnectTip|not allowed' \"$DATADIR/regtest/debug.log\"\n```\n\n[quoted text omitted]\n```bash\n2026-07-28T23:18:57Z [warning] RPC User __cookie__ not allowed to call method getblock\n2026-07-28T23:18:55Z ERROR: ConnectTip: ConnectBlock 0000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef failed, bad-txns-inputs-missingorspent\n```\n\n[quoted text omitted]\n```bash\n2026-07-28T23:19:45Z [warning] RPC User __cookie__ not allowed to call method getblock\\x0a2026-07-28T23:19:41Z ERROR: ConnectTip: ConnectBlock 0000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef failed, bad-txns-inputs-missingorspent\n```",
 "commits": [
  {
   "sha": "33abd9cf79d36854495f8d9dc614d944a194bea9",
   "date": "2026-09-15T18:49:08Z",
   "message": "test: characterize RPC and wallet input logs"
  },
  {
   "sha": "24efc8ecb758ee3146b0fba357f339a287527b3f",
   "date": "2026-09-15T21:05:29Z",
   "message": "log: split multiline messages\n\nAdd SplitLines and use it for intentional multiline exception, message-box, wallet transaction, and wallet database output. Giving each line its own log entry preserves readability when embedded newlines are escaped by the logger.\n\nVisit line views synchronously through a callback to avoid allocating a container.\n\nLog exceptions as ordinary warnings and remove the blank lines and asterisk banner. Each line now has the usual warning prefix, so the separators add noise without diagnostic information. Keep the existing stderr formatting.\n\nSplit wallet database errors too, so descriptor-load diagnostics and their Details line remain separately readable.\n\nCo-authored-by: L\u0151rinc <pap.lorinc@gmail.com>"
  },
  {
   "sha": "54c71d72e842a7286b81db3275368a5f43a8a7df",
   "date": "2026-09-15T21:05:29Z",
   "message": "log: escape newlines in messages\n\nEscape embedded newlines as \\x0a, like other control characters, so user input cannot forge additional log lines. Apply this in the logger to cover RPC methods, wallet names, and other runtime strings. Preserve non-ASCII bytes so valid UTF-8 remains readable.\n\nRemove exactly one trailing newline before escaping because many callers still supply it as the conventional log terminator. Append the terminator centrally, escaping any remaining newlines.\n\nA follow-up could remove conventional trailing newlines from logging call sites and then remove the suffix stripping here."
  }
 ],
 "timeline": [
  {
   "t": "2026-07-29T02:51:57Z",
   "kind": "comment",
   "who": "Crypt-iQ",
   "assoc": "MEMBER",
   "text": "Just posting mostly as a curiosity to see if you looked at whether the logging interface is vulnerable to format string attacks. I don't think it is, but would be interested in knowing if so. Though I'm not aware of anywhere in the p2p interface that can arbitrarily log blobs."
  },
  {
   "t": "2026-07-29T05:58:50Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "I wonder if non-trailing b\"\\n\" can  be wholesale rejected by the log framework, unless they are opt-in for the IIRC single log line that \"needs\" them?"
  },
  {
   "t": "2026-07-29T06:21:53Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "ed4eb51e9fc6f62975e272e96b22e0a6b64d3205"
  },
  {
   "t": "2026-07-29T21:25:51Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "If someone does a followup that addresses that for the whole log framework, please ping me.\nHere I meant to cover the two exceptional cases I found of user-provided data that escapes sanitization."
  },
  {
   "t": "2026-08-04T22:15:49Z",
   "kind": "comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "text": "We also print arbitrary stuff from the tor control server:\n\n+1 to just blocking newlines in all logging, the tor control server can log control sequences except for those disallowed by `LogEscapeMessage`\n\nhttps://github.com/bitcoin/bitcoin/blob/17c5e33e9c5418fb0240f5d4210c87f54b88cdda/src/torcontrol.cpp#L672\n\nand from the i2p daemon:\n\nhttps://github.com/bitcoin/bitcoin/blob/17c5e33e9c5418fb0240f5d4210c87f54b88cdda/src/i2p.cpp#L273-L275\n\nBoth of these can be remote in some setups (but are trusted i.e. if they are compromised you are in big trouble)\n\nOn top of newlines, there are some (obscure i believe) terminal setups where 8-bit control sequences `> 127` can be printed which allows e.g. clipboard injection.\n\nMaybe `LogEscapeMessage` like this:\n\n```diff\ndiff --git a/src/logging.cpp b/src/logging.cpp\nindex bbe5f043ed..39e9493749 100644\n--- a/src/logging.cpp\n+++ b/src/logging.cpp\n@@ -338,7 +338,9 @@ namespace BCLog {\n         std::string ret;\n         for (char ch_in : str) {\n             uint8_t ch = (uint8_t)ch_in;\n-            if ((ch >= 32 || ch == '\\n') && ch != '\\x7f') {\n+            if (ch == '\\n') {\n+                continue;\n+            } else if (ch >= 32 && ch < 127) {\n                 ret += ch_in;\n             } else {\n                 ret += strprintf(\"\\\\x%02x\", ch);\n@@ -427,7 +429,7 @@ std::string BCLog::Logger::Format(const util::log::Entry& entry) const\n     result += GetLogPrefix(static_cast<LogFlags>(entry.category), entry.level);\n     result += LogEscapeMessage(entry.message);\n\n-    if (!result.ends_with('\\n')) result += '\\n';\n+    result += '\\n';\n     return result;\n }\n```"
  },
  {
   "t": "2026-08-05T08:01:45Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nSure, but this will silently break logs, as explained above. Ref:\n\n```\n# git grep --extended-regexp '(WalletLogPrintf\\(\"CommitTransaction:\\\\n|LogWarning\\(\"\\\\n)'\nsrc/util/exception.cpp:    LogWarning(\"\\n\\n************************\\n%s\", message);\nsrc/wallet/wallet.cpp:    WalletLogPrintf(\"CommitTransaction:\\n%s\\n\", util::RemoveSuffixView(tx->ToString(), \"\\n\"));\n```\n\nThey will need to be fixed, or opt-in with an option, as explained above."
  },
  {
   "t": "2026-08-05T08:04:32Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "My recommendation would be to go ahead with this pull first, then rework the log framework in a separate pull."
  },
  {
   "t": "2026-08-05T18:27:23Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "I agree with Marco that the logging framework should be reworked separately, I\u2019d like to keep this PR focused on the identified newline injection paths and handle broader filtering in follow-ups.\nAnd we should probably be careful about escaping every byte above `0x7f`, since that could also affect valid UTF-8, including translated log messages."
  },
  {
   "t": "2026-08-07T23:52:39Z",
   "kind": "comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "text": "ACK ed4eb51e9fc6f62975e272e96b22e0a6b64d3205"
  },
  {
   "t": "2026-08-26T11:11:02Z",
   "kind": "review",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "ed4eb51e9fc6f62975e272e96b22e0a6b64d3205",
   "text": "tACK ed4eb51e9fc6f62975e272e96b22e0a6b64d3205\n\nmanual test\n\n```bash\n$ ./build/bin/bitcoin-cli -regtest createwallet invalid\\nwallet\n{\n  \"name\": \"invalidnwallet\"\n}\nsliv3r@sliv3r-tuxedo:~/Documentos/Projectes/BitcoinCore/bitcoin$ ./build/bin/bitcoin-cli -regtest createwallet \"invalid\\nwallet\"\n{\n  \"name\": \"invalid\\\\nwallet\"\n}\nsliv3r@sliv3r-tuxedo:~/Documentos/Projectes/BitcoinCore/bitcoin$ ./build/bin/bitcoin-cli -regtest createwallet $'invalid\\nwallet'\nerror code: -8\nerror message:\nWallet name cannot contain control characters\n\n```\n\n<\\details>"
  },
  {
   "t": "2026-09-14T19:35:43Z",
   "kind": "review_comment",
   "who": "ryanofsky",
   "assoc": "MEMBER",
   "path": "src/httprpc.cpp",
   "commit": "6ed8e2af39a16885b9192a42229d7a7effba47a2",
   "in_reply_to": null,
   "text": "In commit \"test: characterize rejected RPC method logs\" (9d5fb22f1d499267a1deda6320a417c2b7d03530)\n\nIt doesn't seem ideal to use SanitizeString string here because this strips characters other than newlines, and produce confusing / misleading warnings, and could make it appear that whitelisted methods were being rejected in the logs.\n\nIt would be good (in a followup) to revert this change and simply escape newlines with \\n in log messages, just like we escape all other control characters."
  },
  {
   "t": "2026-09-14T20:00:41Z",
   "kind": "review",
   "who": "ryanofsky",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "ed4eb51e9fc6f62975e272e96b22e0a6b64d3205",
   "text": "Code review ACK ed4eb51e9fc6f62975e272e96b22e0a6b64d3205, but the fixes here seem messy and fragile. Would be better to just escape newlines with `\\n` in log messages like we escape other control characters as others have suggested. Since there are only 3 places in the code using multiline log messages, this should be pretty easy:\n\ndiff\n\n```diff\n--- a/src/logging.cpp\n+++ b/src/logging.cpp\n@@ -330,15 +330,17 @@ namespace BCLog {\n     /** Belts and suspenders: make sure outgoing log messages don't contain\n      * potentially suspicious characters, such as terminal control codes.\n      *\n-     * This escapes control characters except newline ('\\n') in C syntax.\n-     * It escapes instead of removes them to still allow for troubleshooting\n-     * issues where they accidentally end up in strings.\n+     * This escapes control characters, including newline ('\\n'), in C\n+     * syntax, so a message always occupies exactly one line and data that\n+     * ends up in a message can't forge additional log lines. It escapes\n+     * instead of removes them to still allow for troubleshooting issues\n+     * where they accidentally end up in strings.\n      */\n     std::string LogEscapeMessage(std::string_view str) {\n         std::string ret;\n         for (char ch_in : str) {\n             uint8_t ch = (uint8_t)ch_in;\n-            if ((ch >= 32 || ch == '\\n') && ch != '\\x7f') {\n+            if (ch >= 32 && ch != '\\x7f') {\n                 ret += ch_in;\n             } else {\n                 ret += strprintf(\"\\\\x%02x\", ch);\n@@ -425,9 +427,10 @@ std::string BCLog::Logger::Format(const util::log::Entry& entry) const\n     }\n\n     result += GetLogPrefix(static_cast<LogFlags>(entry.category), entry.level);\n-    result += LogEscapeMessage(entry.message);\n-\n-    if (!result.ends_with('\\n')) result += '\\n';\n+    // Many callers still terminate messages with '\\n'. Drop that one trailing\n+    // newline so it isn't escaped, then terminate the line unconditionally.\n+    result += LogEscapeMessage(util::RemoveSuffixView(entry.message, \"\\n\"));\n+    result += '\\n';\n     return result;\n }\n\n--- a/src/noui.cpp\n+++ b/src/noui.cpp\n@@ -8,6 +8,7 @@\n #include <node/interface_ui.h>\n #include <util/btcsignals.h>\n #include <util/log.h>\n+#include <util/string.h>\n #include <util/translation.h>\n\n #include <string>\n@@ -26,18 +27,18 @@ void noui_ThreadSafeMessageBox(const bilingual_str& message, unsigned int style)\n     switch (style) {\n     case CClientUIInterface::MSG_ERROR:\n         strCaption = \"Error: \";\n-        if (!fSecure) LogError(\"%s\\n\", message.original);\n+        if (!fSecure) SplitLines(message.original, [](std::string_view line) { LogError(\"%s\", line); });\n         break;\n     case CClientUIInterface::MSG_WARNING:\n         strCaption = \"Warning: \";\n-        if (!fSecure) LogWarning(\"%s\\n\", message.original);\n+        if (!fSecure) SplitLines(message.original, [](std::string_view line) { LogWarning(\"%s\", line); });\n         break;\n     case CClientUIInterface::MSG_INFORMATION:\n         strCaption = \"Information: \";\n-        if (!fSecure) LogInfo(\"%s\\n\", message.original);\n+        if (!fSecure) SplitLines(message.original, [](std::string_view line) { LogInfo(\"%s\", line); });\n         break;\n     default:\n-        if (!fSecure) LogInfo(\"%s%s\\n\", strCaption, message.original);\n+        if (!fSecure) SplitLines(message.original, [&](std::string_view line) { LogInfo(\"%s%s\", strCaption, line); });\n     }\n\n     tfm::format(std::cerr, \"%s%s\\n\", strCaption, message.original);\n--- a/src/test/util_tests.cpp\n+++ b/src/test/util_tests.cpp\n@@ -1398,8 +1398,10 @@ BOOST_AUTO_TEST_CASE(test_LogEscapeMessage)\n {\n     // ASCII and UTF-8 must pass through unaltered.\n     BOOST_CHECK_EQUAL(BCLog::LogEscapeMessage(\"Valid log message\u8c93\"), \"Valid log message\u8c93\");\n-    // Newlines must pass through unaltered.\n-    BOOST_CHECK_EQUAL(BCLog::LogEscapeMessage(\"Message\\n with newlines\\n\"), \"Message\\n with newlines\\n\");\n+    // Newlines are escaped like other control characters, so a message can't\n+    // span (or forge) log lines. Logger::Format strips the conventional\n+    // trailing newline before escaping.\n+    BOOST_CHECK_EQUAL(BCLog::LogEscapeMessage(\"Message\\n with newlines\\n\"), R\"(Message\\x0a with newlines\\x0a)\");\n     // Other control characters are escaped in C syntax.\n     BOOST_CHECK_EQUAL(BCLog::LogEscapeMessage(\"\\x01\\x7f Corrupted log message\\x0d\"), R\"(\\x01\\x7f Corrupted log message\\x0d)\");\n     // Embedded NULL characters are escaped too.\n--- a/src/util/exception.cpp\n+++ b/src/util/exception.cpp\n@@ -7,6 +7,7 @@\n\n #include <tinyformat.h>\n #include <util/log.h>\n+#include <util/string.h>\n\n #include <exception>\n #include <iostream>\n@@ -36,6 +37,6 @@ static std::string FormatException(const std::exception* pex, std::string_view t\n void PrintExceptionContinue(const std::exception* pex, std::string_view thread_name)\n {\n     std::string message = FormatException(pex, thread_name);\n-    LogWarning(\"\\n\\n************************\\n%s\", message);\n+    SplitLines(strprintf(\"\\n\\n************************\\n%s\", message), [](std::string_view line) { LogWarning(\"%s\", line); });\n     tfm::format(std::cerr, \"\\n\\n************************\\n%s\\n\", message);\n }\n--- a/src/util/string.h\n+++ b/src/util/string.h\n@@ -160,6 +160,23 @@ std::vector<T> Split(std::span<const char> sp LIFETIMEBOUND, char sep, bool incl\n     return Split<std::string>(str, separators);\n }\n\n+/**\n+ * Call fn(line) for each newline-separated line of str, without the newline.\n+ * An empty str produces one empty line; a trailing newline does not produce\n+ * an extra one. Intended for logging multi-line text one log message per line,\n+ * since a log message cannot contain a newline (see LogEscapeMessage).\n+ */\n+template <typename Fn>\n+void SplitLines(std::string_view str, Fn&& fn)\n+{\n+    do {\n+        const size_t pos{str.find('\\n')};\n+        fn(str.substr(0, pos));\n+        if (pos == std::string_view::npos) break;\n+        str.remove_prefix(pos + 1);\n+    } while (!str.empty());\n+}\n+\n [[nodiscard]] inline std::string_view TrimStringView(std::string_view str LIFETIMEBOUND, std::string_view pattern = \" \\f\\n\\r\\t\\v\")\n {\n     std::string::size_type front = str.find_first_not_of(pattern);\n--- a/src/wallet/wallet.cpp\n+++ b/src/wallet/wallet.cpp\n@@ -2127,7 +2127,7 @@ void CWallet::CommitTransaction(\n )\n {\n     LOCK(cs_wallet);\n-    WalletLogPrintf(\"CommitTransaction:\\n%s\\n\", util::RemoveSuffixView(tx->ToString(), \"\\n\"));\n+    SplitLines(strprintf(\"CommitTransaction:\\n%s\", tx->ToString()), [&](std::string_view line) { WalletLogPrintf(\"%s\\n\", line); });\n\n     // Add tx to wallet, because if it has change it's also ours,\n     // otherwise just for transaction history.\n```"
  },
  {
   "t": "2026-09-15T07:34:12Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "aae82ddcc52a2f87a73077d150f9dd95ef7e572d"
  },
  {
   "t": "2026-09-15T07:45:03Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Thanks @ryanofsky, rebased and replaced RPC-specific sanitization with global newline escaping, preserving printable method characters. Intentional multiline logs use the new `SplitLines` (rewritten slightly and covered with tests)."
  },
  {
   "t": "2026-09-15T07:45:46Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/httprpc.cpp",
   "commit": "6ed8e2af39a16885b9192a42229d7a7effba47a2",
   "in_reply_to": 4008833009,
   "text": "Did something similar"
  },
  {
   "t": "2026-09-15T09:19:00Z",
   "kind": "review",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "aae82ddcc52a2f87a73077d150f9dd95ef7e572d",
   "text": "re-ACK aae82ddcc52a2f87a73077d150f9dd95ef7e572d"
  },
  {
   "t": "2026-09-15T13:36:35Z",
   "kind": "review_comment",
   "who": "ryanofsky",
   "assoc": "MEMBER",
   "path": "src/util/exception.cpp",
   "commit": "aae82ddcc52a2f87a73077d150f9dd95ef7e572d",
   "in_reply_to": null,
   "text": "In commit \"log: escape newlines in messages\" (aae82ddcc52a2f87a73077d150f9dd95ef7e572d)\n\nMight be a little better to drop these separating lines for simplicity. If PrintExceptionContinue is called it seems reasonable to just log the exception as a normal warning. No strong opinion though (and the patch I posted did have these)."
  },
  {
   "t": "2026-09-15T14:01:46Z",
   "kind": "review_comment",
   "who": "ryanofsky",
   "assoc": "MEMBER",
   "path": "src/util/string.h",
   "commit": "54c71d72e842a7286b81db3275368a5f43a8a7df",
   "in_reply_to": null,
   "text": "In commit \"log: escape newlines in messages\" (aae82ddcc52a2f87a73077d150f9dd95ef7e572d)\n\nSearching code and looking for \\x0a in test log output showed one more place where I think it makes sense to use `SplitLines`:\n\n- Main one is [walletdb.cpp:501](https://github.com/bitcoin/bitcoin/blob/57721f8074c2e8ccfe4c6d10114d4683dca58118/src/wallet/walletdb.cpp#L501) which can show multline messages from [walletdb.cpp:781](https://github.com/bitcoin/bitcoin/blob/57721f8074c2e8ccfe4c6d10114d4683dca58118/src/wallet/walletdb.cpp#L781). It would seem good to use `SplitLines` there for readability since the message is not shown elsewhere.\n\nOther places where `SplitLines` could be used but I think it's is better to not are:\n\n- [common/init.cpp:90](https://github.com/bitcoin/bitcoin/blob/57721f8074c2e8ccfe4c6d10114d4683dca58118/src/common/init.cpp#L90) which prints a multiline warning, but IMO better not to add `SplitLines` there because normally this text is an `InitError` and users have to explicitly set an option to downgrade it to a warning, so seems good for it to take up less space.\n- [ipc/capnp/protocol.cpp:43](https://github.com/bitcoin/bitcoin/blob/57721f8074c2e8ccfe4c6d10114d4683dca58118/src/ipc/capnp/protocol.cpp#L43) where multliline messages might have been logged before and will now use \\x0a. But in this case I think it would be worse to use `SplitLines`, and \\x0a is an improvement for safety reasons and grepping.\n- [qt/bitcoin.cpp:187](https://github.com/bitcoin/bitcoin/blob/57721f8074c2e8ccfe4c6d10114d4683dca58118/src/qt/bitcoin.cpp#L187) and [src/dbwrapper.cpp:105](https://github.com/bitcoin/bitcoin/blob/57721f8074c2e8ccfe4c6d10114d4683dca58118/src/dbwrapper.cpp#L105), and in tor/i2p prints as pointed out earlier, but these all seem like cases that should not use `SplitLines` because single lines are expected and output isn't necessarily trusted."
  },
  {
   "t": "2026-09-15T14:32:37Z",
   "kind": "review",
   "who": "ryanofsky",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "aae82ddcc52a2f87a73077d150f9dd95ef7e572d",
   "text": "Code review ACK aae82ddcc52a2f87a73077d150f9dd95ef7e572d. Thanks for picking up the escaping approach, this seems simpler than the per-site sanitizing and it covers more cases like the tor/i2p cases @davidgumberg mentioned as well.\n\nI do think it would be a little better to drop the two wallet commits here (316599d3e7d0bcd5e9e7f12e7fa4a3cd57f736f2, c4ef1c0e3d7434c1b274f0eeb2e9fe1e5a55e8cb) and only keep the logging commits (f984c21cd1a8672105c90890c99ecfa7f17ca376, aae82ddcc52a2f87a73077d150f9dd95ef7e572d), just because the logging commits are sufficient to fix the reported problem, and restricting wallet names seems out of scope and something that deserves a wallet PR. But also fine to keep the wallet commits, I know they already received ACKs including from achow."
  },
  {
   "t": "2026-09-15T14:40:19Z",
   "kind": "comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThis might be a good idea, there's already a PR trying to restrict some wallet names, which could do that job if the author wants to: https://github.com/bitcoin/bitcoin/pull/35768\n\nI would be happy doing it here too tho"
  },
  {
   "t": "2026-09-15T21:11:15Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "54c71d72e842a7286b81db3275368a5f43a8a7df"
  },
  {
   "t": "2026-09-15T21:18:31Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Thanks, updated to focus on logging, dropped the wallet-name restrictions, and kept the RPC and wallet log-injection regressions. Wallet names can still contain newlines on platforms that allow them, but restricting them for paths and UIs can be handled independently.\nThe manual reproducer still shows the RPC injection path, but the solution is more general now."
  },
  {
   "t": "2026-09-15T21:21:17Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/util/string.h",
   "commit": "54c71d72e842a7286b81db3275368a5f43a8a7df",
   "in_reply_to": 4016441172,
   "text": "Applied it to https://github.com/bitcoin/bitcoin/blob/57721f8074c2e8ccfe4c6d10114d4683dca58118/src/wallet/walletdb.cpp#L501 but kept https://github.com/bitcoin/bitcoin/blob/24efc8ecb758ee3146b0fba357f339a287527b3f/src/wallet/walletdb.cpp#L782 (let me know if you think we should do this as well, this one seemed less cleanly applicable."
  }
 ],
 "labels_log": [
  {
   "t": "2026-07-29T01:00:56Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-29T07:23:30Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-15T21:15:54Z",
   "action": "labeled",
   "label": "Utils/log/libs",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-09-15T21:15:50Z",
   "kind": "renamed",
   "who": "l0rinc",
   "from": "rpc,wallet: prevent user input injecting fake log lines",
   "to": "log: prevent user input from injecting fake log lines"
  }
 ],
 "text_chars": 18309,
 "text_tokens_estimate": 4577,
 "changed_paths": [
  "src/logging.cpp",
  "src/noui.cpp",
  "src/test/util_tests.cpp",
  "src/util/exception.cpp",
  "src/util/string.h",
  "src/wallet/wallet.cpp",
  "src/wallet/walletdb.cpp",
  "test/functional/rpc_whitelist.py",
  "test/functional/wallet_createwallet.py"
 ],
 "files": [
  {
   "path": "src/logging.cpp",
   "add": 9,
   "del": 7
  },
  {
   "path": "src/noui.cpp",
   "add": 5,
   "del": 4
  },
  {
   "path": "src/test/util_tests.cpp",
   "add": 24,
   "del": 2
  },
  {
   "path": "src/util/exception.cpp",
   "add": 2,
   "del": 1
  },
  {
   "path": "src/util/string.h",
   "add": 11,
   "del": 0
  },
  {
   "path": "src/wallet/wallet.cpp",
   "add": 2,
   "del": 1
  },
  {
   "path": "src/wallet/walletdb.cpp",
   "add": 2,
   "del": 1
  },
  {
   "path": "test/functional/rpc_whitelist.py",
   "add": 14,
   "del": 2
  },
  {
   "path": "test/functional/wallet_createwallet.py",
   "add": 6,
   "del": 0
  }
 ],
 "test_lines": 48,
 "git": {
  "head": "54c71d72e842a7286b81db3275368a5f43a8a7df",
  "head_matches_backup": true,
  "base": "57721f8074c2e8ccfe4c6d10114d4683dca58118",
  "commits": [
   {
    "sha": "33abd9cf79",
    "subject": "test: characterize RPC and wallet input logs",
    "files": 2,
    "add": 20,
    "del": 2
   },
   {
    "sha": "24efc8ecb7",
    "subject": "log: split multiline messages",
    "files": 6,
    "add": 44,
    "del": 7
   },
   {
    "sha": "54c71d72e8",
    "subject": "log: escape newlines in messages",
    "files": 4,
    "add": 13,
    "del": 11
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "d221933e59151ad4",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}