Dear coreutils team,

We found a bug in csplit that loses data when the input file has the name of one of the output files (xx00, xx01, ... by default). csplit creates (and so truncates) each output file before reading the part of the input that goes into it. If the input is one of these files and has not yet been read to the end, the part not yet read is lost.

For xx00 this is all of the input, since nothing has been read yet. For the later output files, how far csplit has read is the previous split point rounded up to its read buffer, so whether data is lost depends on where the input is split, not only on its size. After the truncation, csplit sees an early end of file, and usually exits with status 0. If a later pattern then fails, csplit removes the output files it has created, the input among them.

With regular expressions, both how far csplit has read and which output files are created depend on the input's contents: the split points are where the expressions match, and with a repeat count of {*} the number of output files is the number of matches. A leading %REGEX% only delays the creation of xx00 until the skipped lines have been read.

We reproduced this with versions 8.32 (Ubuntu 22.04.5 LTS), 9.4 (Ubuntu 24.04) and 9.7 (Debian 13). The results below are the same in all three except where noted; byte counts are from 9.4 and 9.7 (8.32 differs by one byte).

This bug was found by João Matos (joaorsmatos02) and Frederico Ramos (frediramos), as part of a research project at Instituto Superior Técnico, University of Lisbon (https://tecnico.ulisboa.pt/) and Carnegie Mellon University (https://www.cmu.edu/).

## Environment

 - Coreutils version: GNU coreutils 9.7
 - Operating system: Debian GNU/Linux 13
 - Architecture: x86_64
 - Locale: C.UTF8 (LANG=C.UTF-8, LC_ALL unset)

## Steps to reproduce

### Case 1: the input is named xx00

$ seq 10 > xx00 && csplit xx00 5

8.32 and 9.4: "csplit: input disappeared", exit status 1, xx00 is left empty. 9.7: "csplit: '5': line number out of range", exit status 1, xx00 is deleted. Where the input is split does not matter.

### Case 2: a later output file, silent loss

$ seq 300000 > xx01 && csplit xx01 2
2
8188

Exit status 0, no message. xx01 now holds 8188 bytes; the input had 1988895.

### Case 3: a later pattern fails, and the input is deleted

$ seq 300000 > xx01 && csplit xx01 2 50000
csplit: '50000': line number out of range
8188

Exit status 1. Both xx00 and xx01 are removed: nothing is left of the input. The same happens with regular expressions:

$ seq 300000 > xx01 && csplit xx01 '/^1000/' '/299990/'
csplit: '/^299990$/': match not found
3888
csplit: '/299990/': match not found
4302

### Case 4: {*}: which files are at risk depends on the contents

$ seq 300000 > xx02 && csplit xx02 '/0000$/' '{*}'

Exit status 0. Only xx00 to xx02 are created, and xx02 holds 5766 bytes. The same command on a file named xx31 loses nothing: there are 30 matches, so only xx00 to xx30 are created.

### Case 5: a leading %REGEX%

$ seq 300000 > xx00 && csplit xx00 '%^5$%' 10
10
8172

Exit status 0. xx00 holds lines 5 to 9 as expected, but xx01, which should hold the rest of the input, holds only 8172 bytes.

## Fix

We have a patch (along with a new test) that does what split already does: opens each output file without O_TRUNC, compares it with the input (same device and inode), and fails with "'xx00' would overwrite input; aborting" before writing anything. The input is left intact.

Should we do it publicly in order to be properly credited? Or would you prefer to open a private branch for us on Github?

Do not hesitate to contact us for any further details.

Best regards,
João and Frederico







Reply via email to