fix: render actual newline in terminal-sanitized error messages

sanitize_for_terminal() escaped every control char it saw, including
plain LF, so any multi-line diagnostic built from a hardcoded template
(e.g. the path-separator error in main.rs) came out on a TTY as the
literal 4-character sequence "\x0A" instead of a line break.

LF cannot draw, move the cursor, or exfiltrate data the way ESC- and
CSI/OSC-initiated sequences can, so it does not need the same defense
as those. CR is still escaped since it can overwrite the current line
and forge fake output.

Closes #2104
This commit is contained in:
dualfroz committed 2026-08-27 23:35:43 +02:00
1 parent b6fe117b17
commit 8157be1243
2 files changed
+29 -6

No files matched your search

+2 -1
View File
@@ -4,7 +4,8 @@
-
## Bugfixes
-
- Render actual newlines instead of the literal `\x0A` escape in multi-line error
messages printed to a terminal, see #2104
# 10.5.0
+27 -5
View File
@@ -3,11 +3,19 @@
use std::borrow::Cow;
use std::fmt::Write;
/// True for any char that is neither printable nor permitted whitespace (only HT).
/// Covers C0/C1/DEL, bidi overrides, zero-width and format chars, and tag chars.
/// True for any char that is neither printable nor permitted whitespace (only HT
/// and LF). Covers C0/C1/DEL, bidi overrides, zero-width and format chars, and tag
/// chars.
///
/// LF is deliberately let through even though it is a C0 control: it only ever
/// starts a new line and cannot draw, move the cursor or exfiltrate data the way
/// ESC- and CSI/OSC-initiated sequences can, so escaping it just mangles the
/// developer-authored multi-line diagnostics fd prints (see sharkdp/fd#2104).
/// CR is still escaped: unlike LF it can overwrite the current line and forge
/// fake output.
#[inline]
fn needs_escape(c: char) -> bool {
if c == '\t' {
if c == '\t' || c == '\n' {
return false;
}
c.is_control()
@@ -107,8 +115,22 @@ mod tests {
}
#[test]
fn strips_newline() {
assert_eq!(sanitize_for_terminal("a\nb"), "a\\x0Ab");
fn preserves_newline() {
assert!(matches!(sanitize_for_terminal("a\nb"), Cow::Borrowed(_)));
assert_eq!(sanitize_for_terminal("a\nb"), "a\nb");
}
#[test]
fn preserves_real_newlines_in_diagnostics() {
// Multi-line diagnostics such as the path-separator error in main.rs use
// real newlines for readability; they carry no attacker-controlled bytes,
// so a bare '\n' must reach the terminal as an actual line break, not as
// the literal 4-character sequence "\x0A". See sharkdp/fd#2104.
let message = "The search pattern 'a/b' contains a path-separation character \
and will not lead to any search results.\n\n\
If you want to search for all files inside the 'a/b' directory, use a match-all pattern:\n\n \
fd . 'a/b'";
assert_eq!(sanitize_for_terminal(message), message);
}
#[test]