On 10/07/2026 17:30, Pádraig Brady wrote:
On 10/07/2026 10:35, Pádraig Brady wrote:
Note I'm also looking at a follow up patch to
disallow or more probably replace ',' and \n in names
when not quoting, to give better protection against
malicious names.
Proposed patches for this attached.
On second thoughts I think this is a bit too much complication
for the protection it provides.

Instead let's keep it simpler and just consider protecting '\n'
in file names that might be output to logs etc.

I'll push the attached to do this later.

cheers,
Padraig
From a694982763283f02dac2ead221ef0ec582c498d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email protected]>
Date: Fri, 10 Jul 2026 15:42:34 +0100
Subject: [PATCH] ls: protect newlines for non terminal output

Add extra protection for newlines that might be output to log files etc.

* doc/coreutils.texi (ls invocation): Expand --literal description.
* src/ls.c (quote_name_buf): Replace '\n' with '?' if appropriate.
* tests/ls/dired.sh: Add a negative test case.
* tests/ls/zero-option.sh: Likewise.
* tests/ls/m-option.sh: Likewise. Also add a positive test case.
* NEWS: Mention the improvement.
---
 NEWS                    |  3 +++
 doc/coreutils.texi      |  6 ++++--
 src/ls.c                | 31 ++++++++++++++++++++++++++++++-
 tests/ls/dired.sh       | 13 +++++++++++++
 tests/ls/m-option.sh    | 13 +++++++++++++
 tests/ls/zero-option.sh |  9 ++++-----
 6 files changed, 67 insertions(+), 8 deletions(-)

diff --git a/NEWS b/NEWS
index 523f0047c..34e026b2b 100644
--- a/NEWS
+++ b/NEWS
@@ -87,6 +87,9 @@ GNU coreutils NEWS                                    -*- outline -*-
   'ls -m' now quotes files names containing commas when appropriate,
   so users can better distinguish separating commas.
 
+  'ls' now replaces newlines in file names if ambiguous with separators.
+  Previously newlines were protected only when outputting to a terminal.
+
   'sort' will now better use available memory and parallel operation
   when reading from unknown sized inputs like pipes.
 
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index 862c938e1..a51740d81 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -8466,8 +8466,10 @@ backslash sequences like those used in C.
 @opindex --quoting-style
 Do not quote file names.  However, with @command{ls} nongraphic
 characters are still printed as question marks if the output is a
-terminal and you do not specify the @option{--show-control-chars}
-option.
+terminal and you do not specify the @option{--show-control-chars} option.
+Also newline characters in file names are printed as question marks
+if ambiguous with separators, irrespective of whether output
+is a terminal or not.
 
 @optItem{ls,-q,}
 @optItemx{ls,--hide-control-chars,}
diff --git a/src/ls.c b/src/ls.c
index b2335b217..faa636211 100644
--- a/src/ls.c
+++ b/src/ls.c
@@ -741,6 +741,11 @@ static struct ignore_pattern *hide_patterns;
    quoting methods pass through control chars as-is.  */
 static bool qmark_funny_chars;
 
+/* True means extra replacement of newline with '?'
+   in cases where a newline in a file name would be
+   ambiguous with newline separators.  */
+static bool qmark_newline;
+
 /* Quoting options for file and dir name output.  */
 
 static struct quoting_options *filename_quoting_options;
@@ -2335,6 +2340,12 @@ decode_switches (int argc, char **argv)
                        ? ls_mode == LS_LS && stdout_isatty ()
                        : hide_control_chars_opt);
 
+  qmark_newline = hide_control_chars_opt != false && eolbyte == '\n'
+                  && ((line_length
+                       && (format == with_commas
+                           || format == many_per_line || format == horizontal))
+                      || format == one_per_line || format == long_format);
+
   int qs = quoting_style_opt;
   if (qs < 0)
     qs = getenv_quoting_style ();
@@ -4503,6 +4514,17 @@ quote_name_buf (char **inbuf, size_t bufsize, char *name,
                                    || qs == shell_always_quoting_style
                                    || qs == literal_quoting_style);
 
+  /* Note we provide extra protection only for newline separators.
+     I.e., we don't bother protecting "double space" or "comma space"
+     separated entries, as for interactive output, shell escape quoting
+     is best to disambiguate, and for programmatic, --zero is best.
+     Extra processing to mark ', ' and '  ' would be overkill
+     as interactively there will still be ambiguities with multi-byte spaces,
+     and marked characters impact programmatic processing anyway.  */
+  bool protect_newline
+    = (! dired && qs == literal_quoting_style
+       && qmark_newline && strchr (name, '\n'));
+
   if (needs_general_quoting != 0)
     {
       len = quotearg_buffer (buf, bufsize, name, -1, options);
@@ -4514,7 +4536,7 @@ quote_name_buf (char **inbuf, size_t bufsize, char *name,
 
       quoted = (*name != *buf) || strlen (name) != len;
     }
-  else if (needs_further_quoting)
+  else if (needs_further_quoting || protect_newline)
     {
       len = strlen (name);
       if (bufsize <= len)
@@ -4530,6 +4552,13 @@ quote_name_buf (char **inbuf, size_t bufsize, char *name,
       quoted = false;
     }
 
+  /* It's safe to replace newlines without multi-byte iteration, as newlines
+     do not appear as part of any multi-byte encoded character.  */
+  if (protect_newline)
+    for (char *p = buf; p < buf + len; p++)
+      if (eolbyte && *p == '\n')
+        *p = '?';
+
   if (needs_further_quoting)
     {
       if (MB_CUR_MAX > 1)
diff --git a/tests/ls/dired.sh b/tests/ls/dired.sh
index 8b80b855b..1becde22d 100755
--- a/tests/ls/dired.sh
+++ b/tests/ls/dired.sh
@@ -76,5 +76,18 @@ while test "$#" -gt 0; do
   index=$(($index + 1))
 done
 
+# Ensure literal quoting preserves newlines for dired consumers.
+newline='n
+l'
+mkdir newline-dir || framework_failure_
+touch "newline-dir/$newline" || framework_failure_
+ls -l --dired --quoting-style=literal newline-dir > out || fail=1
+set -- $(sed -n 's|^//DIRED// ||p' out)
+test "$#" -eq 2 || framework_failure_
+dd bs=1 skip="$1" count="$(($2 - $1))" < out > actual 2>/dev/null \
+  || framework_failure_
+printf %s "$newline" > exp || framework_failure_
+compare exp actual || fail=1
+
 
 Exit $fail
diff --git a/tests/ls/m-option.sh b/tests/ls/m-option.sh
index afe3fd415..b3f8562be 100755
--- a/tests/ls/m-option.sh
+++ b/tests/ls/m-option.sh
@@ -72,4 +72,17 @@ for qs in $(cut -d: -f1 exp); do
 done > out
 compare exp out || fail=1
 
+# Newlines are preserved with unlimited width, where -m does not wrap,
+newline='n
+l'
+touch "$newline" || framework_failure_
+printf '%s\n' "$newline" > exp || framework_failure_
+ls -m -w0 "$newline" > out || fail=1
+compare exp out || fail=1
+
+# Otherwise protect newlines that could be confused with separators.
+printf '%s\n' 'n?l' > exp || framework_failure_
+ls -m "$newline" > out || fail=1
+compare exp out || fail=1
+
 Exit $fail
diff --git a/tests/ls/zero-option.sh b/tests/ls/zero-option.sh
index 8c06b5a04..aa053a0af 100755
--- a/tests/ls/zero-option.sh
+++ b/tests/ls/zero-option.sh
@@ -29,12 +29,11 @@ disallowed_options='-l --dired'  # dired only enabled with -l
 returns_ 2 ls $disallowed_options --zero dir || fail=1
 
 disabled_options='--color=always -x -m -C -Q -q'
+newline='n
+l'
+touch dir/'com,ma' "dir/$newline" || framework_failure_
 LC_ALL=C ls $disabled_options --zero dir >out || fail=1
-tr '\n' '\0' <<EOF >exp
-a
-b
-cc
-EOF
+printf '%s\0' a b cc 'com,ma' "$newline" >exp || framework_failure_
 
 compare exp out || fail=1
 
-- 
2.55.0

Reply via email to