I think the argument is that 'split' prevents you from shooting
yourself in the foot here, so 'csplit' should too.
I see, that makes sense, and I probably agree. I guess I was just too
focused on the word "bug". It seems like a nice improvement request.
Thanks all,
Collin
Agreed, it is better described as an improvement than a bug.
The discrepancy to split is why we though it was worth reporting; split
has had this guard since 8.18, and its output names are documented in
the same way as csplit's.
As Paul suggested, our patch and its test are attached. Let us know if
you would like any changes.
In any case, thanks for taking the report into account.
Best,
João
From 3cfcd8cbc323ace701307f70922dca8861709a06 Mon Sep 17 00:00:00 2001
From: Joao Matos <[email protected]>
Date: Thu, 8 Oct 2026 18:16:07 +0100
Subject: [PATCH 1/2] csplit: don't truncate the input when an output file is
the input
csplit opened each output file with fopen "w", which truncates it,
before reading the part of the input that goes into it. When an
output file name (xx00, xx01, ...) refers to the input file, the part
of the input not yet read was lost: all of it for the first output
file, and for later ones whatever was not yet buffered, usually
silently, with exit status 0:
$ seq 300000 > xx01 && csplit xx01 2 && wc -c xx01
8188 xx01 # was 1988895 bytes
If a later pattern then failed, csplit removed the output files it
had created, the input among them.
Instead, as split does, open the output without O_TRUNC, compare it
with the input, and fail with "would overwrite input; aborting"
before anything is written. The input is left intact, and the
output files created so far are removed, unless -k is given.
* src/csplit.c (in_stat_buf): New static variable.
(main): Set it after opening the input.
(create_output_file): Open the output without O_TRUNC, and fail if it
is the input file; truncate it otherwise.
* NEWS: Mention the bug fix.
---
NEWS | 7 +++++++
src/csplit.c | 46 ++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 51 insertions(+), 2 deletions(-)
diff --git a/NEWS b/NEWS
index 246a12b41..1f14d951c 100644
--- a/NEWS
+++ b/NEWS
@@ -9,6 +9,13 @@ GNU coreutils NEWS -*-
outline -*-
was used, even when the look-up inside the chroot succeeded.
[bug introduced in coreutils-8.23]
+ csplit no longer truncates its input when an output file name refers to
+ the input file. Previously the part of the input not yet read was lost,
+ usually silently, and if a later pattern failed the input was removed
+ along with the other output files. Now csplit fails with "would
+ overwrite input; aborting" and leaves the input intact.
+ [This bug was present in "the beginning".]
+
cut, expand, fold, join, numfmt, unexpand, and uniq, no longer consider
the \u0085 (next line) character as a separator on non-GLIBC platforms.
[bug introduced in coreutils-9.11]
diff --git a/src/csplit.c b/src/csplit.c
index d453afbbf..f1d23ace0 100644
--- a/src/csplit.c
+++ b/src/csplit.c
@@ -146,6 +146,9 @@ static int volatile files_created = 0;
/* Number of bytes written to current file. */
static intmax_t bytes_written;
+/* Status of the input file, to refuse an output file that is the input. */
+static struct stat in_stat_buf;
+
/* Output file pointer. */
static FILE *output_stream = NULL;
@@ -896,6 +899,7 @@ create_output_file (void)
int nfiles = files_created;
bool fopen_ok;
int fopen_errno;
+ bool overwrites_input = false;
output_filename = make_filename (nfiles);
@@ -909,13 +913,48 @@ create_output_file (void)
/* Create the output file in a critical section, to avoid races. */
sigset_t oldset;
sigprocmask (SIG_BLOCK, &caught_signals, &oldset);
- output_stream = fopen (output_filename, "w");
+ /* No O_TRUNC: the output may be the input, not read to the end yet.
+ Truncate only once it is known to be another file. */
+ int fd = open (output_filename, O_WRONLY | O_CREAT | O_BINARY,
+ MODE_RW_UGO);
+ if (0 <= fd)
+ {
+ struct stat out_stat_buf;
+ int err = 0;
+ if (fstat (fd, &out_stat_buf) != 0)
+ err = errno;
+ else if (psame_inode (&in_stat_buf, &out_stat_buf))
+ overwrites_input = true; /* Reported below. */
+ else if (ftruncate (fd, 0) != 0
+ && (S_ISREG (out_stat_buf.st_mode)
+ || S_TYPEISSHM (&out_stat_buf)))
+ err = errno;
+ if (err || overwrites_input)
+ {
+ close (fd);
+ fd = -1;
+ errno = err;
+ }
+ }
+ output_stream = 0 <= fd ? fdopen (fd, "w") : NULL;
+ if (0 <= fd && !output_stream)
+ {
+ int e = errno;
+ close (fd);
+ errno = e;
+ }
fopen_ok = (output_stream != NULL);
fopen_errno = errno;
files_created = nfiles + fopen_ok;
sigprocmask (SIG_SETMASK, &oldset, NULL);
}
+ if (overwrites_input)
+ {
+ error (0, 0, _("%s would overwrite input; aborting"),
+ quoteaf (output_filename));
+ cleanup_fatal ();
+ }
if (! fopen_ok)
{
error (0, fopen_errno, "%s", quotef (output_filename));
@@ -1354,7 +1393,10 @@ main (int argc, char **argv)
xalloc_die ();
filename_space = ximalloc (filename_size);
- set_input_file (argv[optind++]);
+ char const *fname = argv[optind++];
+ set_input_file (fname);
+ if (fstat (STDIN_FILENO, &in_stat_buf) != 0)
+ error (EXIT_FAILURE, errno, "%s", quotef (fname));
parse_patterns (argc, optind, argv);
--
2.47.0.windows.2
From 76e58190dfbd6eb6172cefdcc535f9638070a41b Mon Sep 17 00:00:00 2001
From: Joao Matos <[email protected]>
Date: Fri, 9 Oct 2026 00:27:39 +0100
Subject: [PATCH 2/2] tests: csplit: add a test for the fix in the previous
commit
* tests/csplit/guard-input.sh: New file.
* tests/local.mk (all_tests): Add the new test.
---
tests/csplit/guard-input.sh | 83 +++++++++++++++++++++++++++++++++++++
tests/local.mk | 1 +
2 files changed, 84 insertions(+)
create mode 100755 tests/csplit/guard-input.sh
diff --git a/tests/csplit/guard-input.sh b/tests/csplit/guard-input.sh
new file mode 100755
index 000000000..2f778de99
--- /dev/null
+++ b/tests/csplit/guard-input.sh
@@ -0,0 +1,83 @@
+#!/bin/sh
+# ensure csplit doesn't overwrite input with output.
+
+# Copyright (C) 2026 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ csplit
+
+# Larger than csplit's initial read buffer, so that part of the input
+# is still unread when an output file is created.
+seq 10000 > exp || framework_failure_
+
+# The first output file is created before any input is read:
+# csplit truncated the input and failed with "input disappeared".
+cp exp xx00 || framework_failure_
+returns_ 1 csplit xx00 2 > out 2> err || fail=1
+compare exp xx00 || fail=1
+compare /dev/null out || fail=1
+echo "csplit: 'xx00' would overwrite input; aborting" > experr \
+ || framework_failure_
+compare experr err || fail=1
+
+# A later output file: csplit lost the unread part of the input and
+# still exited successfully. Now the output files already created
+# are removed, but not the input.
+rm -f xx?? || framework_failure_
+cp exp xx01 || framework_failure_
+returns_ 1 csplit xx01 2 > out 2> err || fail=1
+compare exp xx01 || fail=1
+test -f xx00 && fail=1
+echo 2 > expout || framework_failure_
+compare expout out || fail=1
+echo "csplit: 'xx01' would overwrite input; aborting" > experr \
+ || framework_failure_
+compare experr err || fail=1
+
+# With -k the output files already created are kept.
+rm -f xx?? || framework_failure_
+cp exp xx01 || framework_failure_
+returns_ 1 csplit -k xx01 2 > /dev/null 2>&1 || fail=1
+compare exp xx01 || fail=1
+echo 1 > exp00 || framework_failure_
+compare exp00 xx00 || fail=1
+
+# The same file under another name, and as standard input.
+rm -f xx?? || framework_failure_
+cp exp xx01 || framework_failure_
+ln -s xx01 in-symlink || framework_failure_
+ln xx01 in-hardlink || framework_failure_
+returns_ 1 csplit in-symlink 2 > /dev/null 2>&1 || fail=1
+returns_ 1 csplit in-hardlink 2 > /dev/null 2>&1 || fail=1
+returns_ 1 csplit - 2 < xx01 > /dev/null 2>&1 || fail=1
+compare exp xx01 || fail=1
+
+# Output file names from --prefix and --suffix-format.
+rm -f xx?? || framework_failure_
+cp exp in-01 || framework_failure_
+returns_ 1 csplit -f in- in-01 2 > /dev/null 2>&1 || fail=1
+compare exp in-01 || fail=1
+cp exp xx-1 || framework_failure_
+returns_ 1 csplit -b '-%d' xx-1 2 > /dev/null 2>&1 || fail=1
+compare exp xx-1 || fail=1
+
+# An existing output file that is not the input is still truncated.
+rm -f xx?? || framework_failure_
+printf '%1000s\n' x > xx00 || framework_failure_
+csplit exp 2 > /dev/null || fail=1
+compare exp00 xx00 || fail=1
+
+Exit $fail
diff --git a/tests/local.mk b/tests/local.mk
index 06d8f934d..28b81b668 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -348,6 +348,7 @@ all_tests = \
tests/csplit/csplit-1000.sh \
tests/csplit/csplit-heap.sh \
tests/csplit/csplit-io-err.sh \
+ tests/csplit/guard-input.sh \
tests/csplit/csplit-suppress-matched.pl \
tests/date/date-debug.sh \
tests/date/date-ethiopia.sh \
--
2.47.0.windows.2