[Toybox] cp --sparse=
Rob Landley
rob at landley.net
Tue Feb 17 14:08:53 PST 2026
On 2/13/26 15:10, enh wrote:
> yeah, given that the "is there at least one hole?" test is just a
> single lseek(), it's odd to me that it's not the coreutils default. i
> feel like "preserve holes" and "make holes wherever possible" are two
> unrelated behaviors.
https://github.com/mpe/linux-fullhistory/commit/982d816581ee is only
from 2011, and I'm assuming "hurd" doesn't support it anyway.
Doing an lseek(SEEK_HOLE) on each source file for auto mode works for
me. Shouldn't be that expensive, and if there is it's sort of prefetch.
(Not that I've benched it, but you're not going find a faster way to get
this data so the alternative is DON'T get this data. Which would be
"defer asking the question until you've read an aligned page of zeroes",
which means not doing sendfile()...)
And if you want to get nasty about it you can use the return value to
posix_fallocate() the destination chunk you're about to write into,
which is uncomfortable for a bunch of reasons. (If you hit ctrl-c at
exactly the wrong time you can wind up with a 0 byte file taking up a
couple gigabytes of disk space. As fallout from an optimization meant to
save storage. No I am not adding a signal handler to call truncate(),
let's just not go there.)
Grumble grumble simple implementation...
>> Eh, maybe. The point of xsendfile() is to wrap the kernel's
>> copy_file_range() stuff for speed, and you'd THINK that would
>> automatically do sparseness if the original was sparse but apparently
>> not. (And that's before "what does macos do"...)
>
> /me checks man page
>
> If fd_in is a sparse file, then copy_file_range() may expand any
> holes existing in the requested range. Users may benefit from
> calling copy_file_range() in a loop, and using the lseek(2)
> SEEK_DATA and SEEK_HOLE operations to find the locations of data
> segments.
>
> d'oh!
You think that's bad, look at man 2 splice. (Why do you CARE that one
end is a pipe? Just DO it, will you? I have wanted "connect these two
filehandles together and let the process exit so the pipeline continues
PAST it" for DECADES (it would make netcat so much easier, it would mean
tar didn't have to keep a second process around shoveling data once it
had autodetected the type of a piped file...) and every time I brought
it up on the kernel list they went "oh no, that's crazy". They
eventually implemented mount --move and eventually gave us punch_hole()
and the ability to find holes, but exeve(NULL, argv, envp) to re-exec
the current running process without requiring /proc/self/exe to be
accessible? That's crazy talk. Let's once again try to remove vfork()
because we don't understand what it's for...)
Grumble grumble. Gotta look at netbsd...
>> Would there be any other users of a sparse copy function, given that tar
>> isn't doing fd->fd copying but always has an archive format at one end?
>
> (given that you've reused the cp code for stuff like install, you're
> probably right that this is the only likely user.)
There's a balance between bundling multiple commands into one command.c
and leaking implementation details into lib/ and so far both answers are
wrong. :(
(There are too many sh builtins in the same file, ps.c desperately needs
breaking up... but lib hasn't got access to TT...)
>>> and i _also_ haven't thought hard
>>> enough about why tar.c's sparse file handling is a two-pass algorithm,
>>> and whether that's meaningful for cp too.
>>
>> Not sure what you mean: sendfile_sparse() is used by the extraction
>> code, it loops over an input array and does the thing, I think in one go?
>
> it was the "there's a TT.sparse array passed in" part that i meant.
When extracting, the sparseness is remembered from tar metadata so the
packed contents can be distributed accordingly. When creating, the
sparseness needs to be recorded in the tar metadata before we go back
and pack up the scattered data.
The two passes are because the sparse info is saved in the headers
before the data, not inline with the data. They occur physically
separated in the file.
>> Would you like cp to auto-sparse files, or only with -a, or...?
>
> i hadn't really thought about the auto-sparsing side. but aiui you're
> saying coreutils _does_ try to do that,
No, I was assuming you were more familiar with this command than I was
and could just tell me what you need. "What gnu does" starts with
reading the man page more closely, which I have now done, and "auto" is
the default, both "never" and "always" must be explicitly requested, and
things like -a don't affect this.
Which is very gnu.
> it's the preservation that it
> doesn't do? that seems like it's doing the _more_ expensive thing? (or
> they have some weird assumption that the cpu time for scanning each
> block is cheap but one failed lseek() is expensive?)
The gnu/dammit project was tantrumed into existence in 1983, linux hole
detection support went in ~30 years later. They never went back and
cleaned things up because learning better is against the gnu/philosophy.
> (fwiw, it was the "lack of hole preservation by default" that caught
> people out. i don't think they had any expectations about adding holes
> that didn't already exist.)
The sane thing would be cp's default is --sparse=never and then cp -a or
cp -p added --sparse=auto but of course that's not what gnu did. Sigh...
Both -s and -S are used, no logical short opt for --sparse. Grumble...
>> Since this is already using sendfile, the likely thing to do is make a
>> sparse_sendfile() that calls sendfile_len() so we get the in-kernel copy
>> of the segments. Except the OTHER fun thing here is we may be copying
>> OVER existing files (I don't remember if tar always just deletes and
>> recreates, or I just decided not to care there). But "cp file.img
>> /dev/fda" was definitely a thing back in the day.
>>
>> So the NEXT question is what do you do if the old and new files don't
>> agree about sparseness? You can:
>>
>> A) seek past annyway and leave old data in place if there was any,
>> B) fallocate(PUNCH_HOLE) the holes, which apparently can fail
>> C) write zeroes to blank any not previously sparse dest data, but the
>> result isn't sparse in those places
>> D) delete dest file if source is sparse and recreate it with lseek
>> E) think harder about what to do
>
> huh ... i also hadn't thought of that. are there existing cases where
> toybox (or coreutils) look at the _destination_ file rather than just
> assuming the source is canonical?
I hadn't tested what coreutils does, I was just asking what _should_
happen when overwriting an existing destination file. ("What gnu does"
is quite often insane, and if we don't NEED to be slavishly crazy I'd
rather not be influenced by their design choices before hearing what the
user actually wants.)
Probably the correct behavior is to truncate any existing destination
file immediately (since we're going to overwrite all of it, we just make
an effort to retain the same inode and fuck up hardlinks). If the
truncate fails fall back to the "copy zeroes" behavior, because we're
writing to a block device or a pipe or something* that _can't_ represent
sparseness in the result. (The --auto vs --always difference is about
what properties of the SOURCE file indicate holes. The destination
either has holes or it doesn't, there's no "some real runs of zeroes and
some holes" option I've noticed, and I dunno why you'd want that.)
* Speaking of pipes, did you know that "man 2 posix_fadvise()" says
EINVAL comes from a bad "advice" value, but in practice the kernel
_also_ returns that if you call it on a filehandle from /dev/urandom?
Yes I poked Alejandro Colomar. He asked me to send a patch. I mention
this because the man page documents ESPIPE as another thing to expect,
but of course that's not what the kernel returns.
> (even if you're ignoring that, your case B is still relevant: "is it a
> failure to copy if you copy a sparse file but can't make the copy
> sparse?".)
No, but possibly you'd emit a warning? I remember back in the day "cp
thingy /mnt/fatpartition" used to complain about losing metadata. (Or
was that tar?)
I kinda lean against the warning though: it's not REALLY an error, and
there's already a warning about running out of space. (The main reason
NOT to automatically turns runs of zeroes into sparse files is so you
don't get disk full errors updating the middle of a file. That's why
losetup cared back in the day. No idea what qemu does for -hda when that
happens, probably a panic exit...)
How about if you _explicitly_ ask for sparse preservation (by saying
--sparse=auto or --sparse=always) and it can't, then warn. But if it's
just the default, best effort and fall back silently.
Rob
More information about the Toybox
mailing list