Skip to content

uucore: drop the pipe after a failed splice write - #15086

Open
abendrothj wants to merge 1 commit into
uutils:mainfrom
abendrothj:pipes-discard-leftover
Open

abendrothj wants to merge 1 commit into
uutils:mainfrom
abendrothj:pipes-discard-leftover

Conversation

@abendrothj

@abendrothj abendrothj commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

cat, cp, install, mv, head, tail and tac copy through cached pipes that get reused for more than one copy. If a write to the destination failed part-way (ENOSPC, EFBIG), the bytes still in the pipe stayed there, and the next copy wrote them at the start of its own destination. With cp, a file that failed to copy put the rest of its data into the next file.

A cached pipe is now put back only when the copy through it succeeded. After a failure it's dropped, and the next copy makes a new one. test_cp_failed_write_does_not_spill_into_next_file fails on main and passes here.

tee has the same problem with its own pipes; that's #15132.

Closes #15085

Copilot AI balanced review requested due to automatic review settings October 5, 2026 10:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@oech3

This comment was marked as resolved.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated
match splice(pipe, dest, len) {
Ok(s) => len -= s,
Err(e) => {
let _ = std::io::copy(&mut pipe.take(len as u64), &mut std::io::sink());

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait... it is probally read(2) to user space RAM. Though std might optimize it with splice to /dev/null in the future... We would not see EIO or OOM at here. But I still think we should discard & regenerate pipe to avoid it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sink() doesn't open anything: it's a unit struct whose write just returns the length, so this is a read into a buffer that gets thrown away.

I'd still drain rather than recreate the pipe. tee also calls drain_pipe, with its own two pipes that aren't in PIPE_CACHE, so dropping the cached pipe wouldn't fix it, and tee has the same bug: on main, when one output fails part-way, the bytes it didn't take end up in the other outputs. test_tee_failed_output_does_not_spill_into_others in 55a4ca3 covers that. Draining in drain_pipe fixes every caller in one place, and either way it only happens after a failed write.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recalling pipe is much cheaper than repeating read internally. At least, we should have .unwrap() to catch failure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, switched to recreating the pipe, so there's no read and nothing to unwrap. tee replaces the pipe a failed output was using with a new one of the same size.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A correction to my reply above: tee doesn't make the new pipe "the same size" anymore. The 2nd pipe is always 1 MiB and the 1st is only enlarged if the 2nd was. The old pair is closed before the new one is made, and if no new pipe can be made tee falls back to read/write. That's in #15132 now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correction: in #15132 tee no longer falls back to read/write, if it can't make a new pipe it reports the error and stops.

Comment thread tests/by-util/test_cp.rs Outdated
fn ignore() -> Self {
// SAFETY: signal() with SIG_IGN is async-signal-safe and `drop` puts
// the old handler back.
Self(unsafe { libc::signal(libc::SIGXFSZ, libc::SIG_IGN) })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you rewrite the test with strace to drop unsafes?

@xtqqczze xtqqczze Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the unsafe operations are properly encapsulated in SigxfszGuard I think unsafe is fine for FFI.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer strace also for readability.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It matches the approach used in tests/by-util/test_dd.rs; I think it’s OK for this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests have no unsafe now: 4179394 adds UCommand::ignore_sigxfsz(), which ignores SIGXFSZ in the child's pre_exec, next to the existing limit and umask ones. The old guard changed SIGXFSZ for the whole test binary, and test_dd's two tests that used it were already killing each other's dd under cargo test (143 of 300 runs with two threads). They use the new method now too.

I tried strace with cp as well: inject=splice:error=EIO:when=4 with --reflink=never reproduces it reliably. But strace isn't in the runner images, and only the script-check jobs install it, so in the test jobs it would return early without checking anything. The fsize limit runs on every Linux job.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think 4179394 deserves it's own PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split out into #15131 (c44a370). This PR is based on it, because the cp test uses ignore_sigxfsz. A correction to my earlier numbers: the 143/300 came from a different setup. With --test-threads=2 on the two dd Fsize tests, main fails 26 of 1000 runs and the new method 0 of 1000.

@codspeed

codspeed Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 2 benchmarks

⚠️ 120 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

ℹ️ 1 benchmark has no flame graph

The flamegraph could not be generated because too much time was spent in runtime internals, making the visualization not useful for performance analysis.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 2 regressed benchmarks
✅ 398 untouched benchmarks
⏩ 54 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Simulation five_38_bit_primes 1.6 s 1.7 s -8.22%
❌ Simulation thirteen_39_bit_primes 9.1 s 9.6 s -4.62%
⚡ Simulation three_39_bit_primes 583.2 ms 429.8 ms +35.69%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing abendrothj:pipes-discard-leftover (ec9a55a) with main (6292c03)

Open in CodSpeed

Footnotes

  1. 54 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@oech3

This comment was marked as outdated.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated
/// `dest` fails part-way, the bytes still in `pipe` are read out and dropped
/// before the error is returned, so that they do not end up at the start of
/// the next destination.
#[inline]

@xtqqczze xtqqczze Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#[inline] shouldn't be necessary for a function that isn't public.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gone along with splice_all.

@oech3

This comment was marked as outdated.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated
/// before the error is returned, so that they do not end up at the start of
/// the next destination.
#[inline]
fn splice_all(pipe: &PipeReader, dest: &impl AsFd, mut len: usize) -> std::io::Result<()> {

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 55a4ca3: the loops are back as they were, with .inspect_err(|_| discard(pipe, remaining))?. I kept a one-line discard so the drain is written once.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is outdated now: there's no discard and no draining after a failed write anymore. The pipe is dropped instead, so the loops don't need an inspect_err at all.

@abendrothj
abendrothj force-pushed the pipes-discard-leftover branch from cf614c3 to 87ceeff Compare October 5, 2026 21:11
Copilot AI balanced review requested due to automatic review settings October 5, 2026 21:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated
Comment on lines +57 to +60
///
/// If writing to `dest` fails part-way, the bytes still in `pipe` are read out
/// and dropped before the error is returned: callers reuse their pipes, and
/// those bytes must not end up at the start of the next destination.

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it under the loop in 7053f9b.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: in dadbe4d the comment sits directly above the loop it's about, and the function doc states the rule for callers.

Comment thread tests/by-util/test_tee.rs Outdated
// When writing to one output fails part-way, the bytes it did not take must
// not end up in the other outputs.
#[test]
fn test_tee_failed_output_does_not_spill_into_others() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One utility is enough to avoid regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the pipe recreated, tee has its own code for this (replace_pipe), which the cp test doesn't reach, so I kept the tee test and made it go through both of tee's pipes. It fails if either one isn't replaced.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR now only has the cp test, test_cp_failed_write_does_not_spill_into_next_file. The tee tests went to #15132 with the tee change, since that's the code they exercise.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated

/// Read out and drop the `len` bytes left in `pipe`.
fn discard(pipe: &PipeReader, len: usize) {
let _ = std::io::copy(&mut pipe.take(len as u64), &mut std::io::sink());

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 7053f9b: both caches are a thread-local Cell now. The pipe is taken out for the copy and put back only if the copy succeeded, so after a failed write it's dropped and the next copy makes a new one. discard is gone. One side effect: if creating the pipe fails, the next call tries again instead of using the fallback for the rest of the process.

@abendrothj
abendrothj force-pushed the pipes-discard-leftover branch from 87ceeff to 55a4ca3 Compare October 5, 2026 21:26
Copilot AI balanced review requested due to automatic review settings October 5, 2026 21:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@abendrothj
abendrothj force-pushed the pipes-discard-leftover branch from 55a4ca3 to 7053f9b Compare October 5, 2026 22:14
Copilot AI balanced review requested due to automatic review settings October 5, 2026 22:14
@abendrothj abendrothj changed the title uucore: drop leftover pipe data after a failed splice write uucore: drop the pipe after a failed splice write Oct 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread src/uu/tee/src/tee.rs Outdated
#[cfg(any(target_os = "linux", target_os = "android"))]
if let Ok((pipe_read, pipe_write)) = io::pipe()
&& let Ok((pipe2_read, pipe2_write)) = io::pipe()
if let Ok(mut pipe) = io::pipe()

This comment was marked as resolved.

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, and no * is needed since they're plain locals: the names are back (pipe_read/pipe_write, pipe2_read/pipe2_write), and the macro reassigns them with (r, w) = .... This is in the tee PR now, #15132.

Comment thread src/uu/tee/src/tee.rs Outdated
#[cfg(any(target_os = "linux", target_os = "android"))]
fn replace_pipe(pipe: &mut (io::PipeReader, io::PipeWriter)) -> io::Result<()> {
use rustix::pipe::{fcntl_getpipe_size, fcntl_setpipe_size};
let size = fcntl_getpipe_size(&pipe.0)?;

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace_pipe and the F_GETPIPE_SZ are gone (in #15132). A new 2nd pipe always comes from pipe::<true>(). The 1st one doesn't, because tee only grows its pipes when growing the 2nd one worked at startup, and growing the 1st can fail on its own when the user's pipe pages run out, which would make tee abort where it now goes on. So a new 1st pipe is pipe::<false>() when the 2nd is 1 MiB, and plain io::pipe() otherwise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

outdated too: in #15132 the macro now makes both new pipes the same way, enlarged only if the 2nd was at startup, so a new 1st pipe that can't be enlarged stops tee as well.


/// splice `len` bytes from `pipe` into `dest`.
///
/// On error, `pipe` may still hold bytes that were not written: don't reuse it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make drain_pipe itself safe instead of asking to reflesh pipe?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept the contract: drain_pipe can leave bytes behind after a partial splice error, its doc says not to reuse the pipe after an error, and the owners act on that. The cache only gets a pipe back when the copy succeeded, and tee replaces the pair a failed output used (that's in #15132). Making it safe in place means discarding the rest on error, which is more I/O on the error path that can fail too, so we'd still need the drop as a backstop. It also wouldn't cover the later-chunk loop in splice_unbounded_with, which calls splice directly and not drain_pipe.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. It seems best currently. I have no idea how to make code simpler.

Comment thread tests/by-util/test_dd.rs

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to #15131. This PR no longer changes test_dd.rs, apart from carrying that commit as its base until it lands.

@oech3

This comment was marked as resolved.

Copilot AI balanced review requested due to automatic review settings October 6, 2026 07:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated
static PIPE_CACHE: OnceLock<Option<(PipeReader, PipeWriter)>> = OnceLock::new();
let Some((pipe_rd, pipe_wr)) = PIPE_CACHE.get_or_init(|| pipe::<false>().ok()) else {
thread_local! {
static PIPE_CACHE: Cell<Option<(PipeReader, PipeWriter)>> = const { Cell::new(None) };

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, moved it out so both use it.

Copilot AI balanced review requested due to automatic review settings October 6, 2026 09:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI balanced review requested due to automatic review settings October 6, 2026 23:47
@abendrothj
abendrothj force-pushed the pipes-discard-leftover branch from cff190a to 054e5ef Compare October 6, 2026 23:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread tests/by-util/test_cp.rs Outdated
Comment on lines +3298 to +3301
#[cfg_attr(
wasi_runner,
ignore = "WASI: the file size limit also applies to wasmtime, which then fails to start"
)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[cfg_attr(
wasi_runner,
ignore = "WASI: the file size limit also applies to wasmtime, which then fails to start"
)]
#[cfg(not(wasi_runner))] // linux specific

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd keep cfg_attr(wasi_runner, ignore = ...), as wasi.yml documents and the rest of test_cp.rs does. It's not about linux (the cfg above handles that): the size limit also applies to wasmtime.

@oech3 oech3 Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, this should no be compiled since it is strictly linux specific test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated
Comment on lines +19 to +20
/// An empty pipe kept for the next copy. A copy takes it and puts it back only
/// when it succeeded, since a failed write can leave bytes in it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// An empty pipe kept for the next copy. A copy takes it and puts it back only
/// when it succeeded, since a failed write can leave bytes in it.
/// Cache empty pipe pair to avoid calling `pipe2` at each copy.

Document usefullness of cache itself. Warning about non-enpty pipes are already done at plate it is used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated
return Ok(Err(()));
};
// GNU cat catches all strace injections for 2nd+ splice
// if this fails part-way, the rest stays in the pipe, so callers drop the pipe on error

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// if this fails part-way, the rest stays in the pipe, so callers drop the pipe on error

Doc of fn itself might enough. We actually have L75's read_to_end too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated
res
}

fn splice_unbounded_with(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fn is used just once. How about removing it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, inlined. A labeled block keeps one place that caches the pipe, and errors still return without it.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated
res
}

fn send_n_bytes_with(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fn is used just once. How about removing it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, inlined. It caches the pipe at its two exits.

Copilot AI balanced review requested due to automatic review settings October 8, 2026 01:18
@abendrothj
abendrothj force-pushed the pipes-discard-leftover branch from 054e5ef to 6f612c0 Compare October 8, 2026 01:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI balanced review requested due to automatic review settings October 10, 2026 04:26
@abendrothj
abendrothj force-pushed the pipes-discard-leftover branch from 6f612c0 to 5f22581 Compare October 10, 2026 04:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.


thread_local! {
/// Cache empty pipe pair to avoid calling `pipe2` at each copy.
static PIPE_CACHE: Cell<Option<(PipeReader, PipeWriter)>> = const { Cell::new(None) };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the purpose of the const block here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It lets thread_local! skip the lazy initialization check; clippy's missing_const_for_thread_local asks for it.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated
pub fn splice_unbounded_auto(source: &impl AsFd, dest: &mut impl AsFd) -> PipeRes {
static PIPE_CACHE: OnceLock<Option<(PipeReader, PipeWriter)>> = OnceLock::new();
let Some((pipe_rd, pipe_wr)) = PIPE_CACHE.get_or_init(|| pipe::<false>().ok()) else {
let Ok(pipe) = PIPE_CACHE.take().map_or_else(pipe::<false>, Ok) else {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let Ok(pipe) = PIPE_CACHE.take().map_or_else(pipe::<false>, Ok) else {
let Ok(&(pipe_rd, pipe_wr)) = PIPE_CACHE.take().map_or_else(pipe::<false>, Ok) else {

Is this possible to remove let (pipe_rd, pipe_wr) = &pipe;?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, by value: take() gives the pair itself, so &(..) doesn't match. Same in send_n_bytes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion, much cleaner.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated
if drain_pipe(pipe_rd, dest, n)?.is_err() {
return Ok(Err(()));
// an error returns without caching the pipe: a failed write can leave bytes in it
let res = 'copy: {

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: the loop moved into the Ok(n) arm, so the match gives the result and there's one PIPE_CACHE.set after it.

Comment thread src/uucore/src/lib/features/pipes.rs Outdated
let (broker_r, broker_w) = &pipe;
let mut n = n;
let mut bytes_written: u64 = 0;
// an error returns without caching the pipe: a failed write can leave bytes in it

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, and the same in splice_unbounded_auto.

@abendrothj
abendrothj force-pushed the pipes-discard-leftover branch from 5f22581 to 1a35ff9 Compare October 11, 2026 06:26
Copilot AI balanced review requested due to automatic review settings October 11, 2026 06:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

Skipping an intermittent issue tests/date/resolution (passes in this run but fails in the 'main' branch)

cat, cp, install, mv, head, tail and tac move data through pipes that are
reused for more than one copy. When writing to the destination failed
part-way, for example with ENOSPC or EFBIG, the bytes still in the pipe
stayed there and the next copy wrote them at the start of its own
destination. With cp, a file that failed to copy put the rest of its data
into the next file copied.

Keep a cached pipe only when the copy through it succeeded, so one that a
failed write left data in is dropped and the next copy makes a new one.
Copilot AI balanced review requested due to automatic review settings October 12, 2026 10:31
@abendrothj
abendrothj force-pushed the pipes-discard-leftover branch from 1a35ff9 to ec9a55a Compare October 12, 2026 10:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cp: after a write error, the next file copied can contain data from the failed one

4 participants