The read scratch had no alignment guarantee beyond
`align_of::<u8>() == 1`. Block devices that enforce a strict
`dma_alignment` (e.g. `sd`, `loop`, `virtio_blk`, `nbd`, `zram`, which
all default to 511) reject O_DIRECT reads whose user buffer is below
that alignment with EINVAL.
Wrap the read scratch in `AlignedBuf`, which over-allocates the zeroed
buffer from `alloc_copy_buffer` by one 4 KiB alignment unit and slices
at the first aligned byte, so the pages still arrive zeroed from the
kernel and are not faulted in before the first read. No `unsafe`
involved.
`fill_consecutive` and `fill_blocks` now take `&mut [u8]` and report
the buffer length (read bytes for the former; reads + padding for the
latter) so the caller can slice without `Vec::truncate`. `read_helper`
returns the data slice directly: into `read_buf` for the common path,
or into a separate `Vec` scratch for `conv=block` / `conv=unblock`,
which can change the byte count and so cannot be done in place. This
also removes the truncate-and-regrow cycle that used to re-fault the
buffer after a short read.
Taken over from #12143 by Christoph Böhmwalder: rebased, storage
switched from `#[repr(align)]` pages to over-allocate-and-offset on
top of `alloc_copy_buffer`, and unit tests for `AlignedBuf` plus an
integration test reading a regular file with `iflag=direct` added.
Closes#12143.
Closes#12085.
Fixes https://bugs.launchpad.net/ubuntu/+source/rust-coreutils/+bug/2125968
The copy buffer was reserved and then filled with BUF_INIT_BYTE, so every
page of bs= was written before the first read. A bs= far larger than the
data being copied therefore cost its full size in resident memory and in
the time to write it, even when the input was empty.
Read::read does need an initialised slice, but zeroed pages are free:
vec![0; n] allocates through alloc_zeroed, so the pages come from the
kernel already zero and are never touched until something is read into
them. The reservation is kept ahead of it so that an unobtainable bs=
still reports an error instead of aborting.
The Windows build never creates a temporary directory: build.rs enables
feat_external_libstdbuf off Unix and the DLL comes from the Cygwin
package. Saying spawn() is there for the TempDir destructor only
described the Unix half of a shared path.
PathBuf lost its import when the exec() use was removed, and the 0700
tempdir helpers were gated on the feature alone rather than on Unix.
The external-libstdbuf path no longer creates a temp directory it
immediately throws away, and waiting on the child now maps a signal
death to 128+signal so the caller still sees what exec() would have
shown it.
Point at rust-lang/cargo#8317 as the still-open request for cargo install
to support installing shared libraries, since that's the root cause of
the /tmp fallback described in the comment above.
Co-authored-by: Cursor <cursoragent@cursor.com>
Open the injected shared library with an explicit mode so umask 0 cannot
leave a world-writable .so in the private temp directory.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use tempfile::Builder::permissions so the directory is never world-accessible
between tempdir() and chmod, closing a TOCTOU window on permissive umasks.
Co-authored-by: Cursor <cursoragent@cursor.com>
Two security/correctness issues reported in #13939:
1. Leaked temporary directories: using exec() replaced the stdbuf
process before TempDir's destructor could run, leaving one
.tmp* directory per invocation in $TMPDIR forever.
Fix: use spawn() + wait() so the parent process survives to drop
the TempDir after the child exits.
2. World-readable temporary directory: the tmpdir was created with
default permissions, making libstdbuf.so writable by any user on
a multi-user system (privilege-escalation risk).
Fix: call set_permissions(0o700) immediately after creation,
bypassing the umask.
Both fixes apply only when feat_external_libstdbuf is not set
(i.e. the embedded .so path).
A dev/ino mismatch between the lstat and the opened fd isn't necessarily
a symlink swap: a directory replaced by another directory, or an
automount that lstat didn't trigger, lands there too. Report that the
file changed instead of pointing at a symlink that doesn't exist.
`safe_remove_dir_recursive` lstats the operand, finds a directory, then
re-resolves the same path with `DirFd::open(.., Follow)`. An operand replaced
by a symlink between the two is followed, and the recursive delete runs over
a tree the user never named.
Open `NoFollow`, and reconcile the opened fd's dev/ino against the lstat:
`O_NOFOLLOW` rejects a symlink only as the final component, and a trailing
slash ("dir/") stops it being final.
The raw syscall(SYS_fchmodat2, ...) in DirFd::chmod_at bypassed LD_PRELOAD
tools (fakeroot, fakechroot, pseudo), so a recursive mode change landed on
disk but their bookkeeping never saw it, silently and with exit status 0.
Try libc fchmodat() first and keep the raw syscall as a fallback for the
NoFollow case libc declines; glibc issues fchmodat2 from there anyway.
Add util/check-libc-interposition.sh, which runs chown -R and chmod -R
under fakeroot and compares what fakeroot reports afterwards.
Fixes#14028
write_padded padded with write!("{: <padlen$}"), which panics once the
dynamic width exceeds u16::MAX. Write the padding spaces directly as bytes so
that %s and %c field widths above that bound work, resolving the existing TODO
in the string arm. Adds a regression test.
Fixes#12593. Fixes#12900.
GNU numfmt rejects a number with a leading '+' sign ("+5") as an invalid
number, and rejects scientific notation ("1e-3") as an invalid suffix in
input. Rust's i128 and f64 parsers accept both, so numfmt silently accepted
them and printed a wrong value with exit 0. Reject them in parse_number_part,
mirroring GNU's two distinct diagnostics.
GNU pr treats -n/--number-lines as optional: a bare -n numbers the lines with a
5-wide, tab-separated line number. uutils required a value, so `pr -t -n` failed.
Set num_args(0..=1); the existing fallback supplies the default numbering mode.