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.
An unknown conv=/iflag=/oflag= flag was met with a help line about
separating flags with commas, when the commas were fine and the flag
name was not. Each now labels the span and lists what it accepts.
An invalid oflag= flag was reported as an input flag, and the rest read
like debug output: "Unrecognized conv=CONV -> zap" where GNU says
"invalid conversion: 'zap'". The messages now match GNU byte for byte,
usage hint included.
A size past u64 saturated to u64::MAX instead of failing, so count= and
skip= took a bogus value: dd copied the whole input and exited 0 where
GNU rejects the operand.