Skip to content

refactor(cp): replace println! with write_stdout_line for robust error#9901

Closed
mattsu2020 wants to merge 17 commits intouutils:mainfrom
mattsu2020:misc/close-stdout.sh
Closed

refactor(cp): replace println! with write_stdout_line for robust error#9901
mattsu2020 wants to merge 17 commits intouutils:mainfrom
mattsu2020:misc/close-stdout.sh

Conversation

@mattsu2020
Copy link
Contributor

@mattsu2020 mattsu2020 commented Dec 29, 2025

Introduce a new write_stdout_line function that locks stdout and returns a Result to handle write failures gracefully. Replace all println! calls in verbose output, debug messages, and path printing with this function, propagating errors via CopyResult<()> instead of panicking on I/O errors. This improves reliability when stdout is closed or encounters issues during cp operations.

related
misc/close-stdout.sh

@github-actions
Copy link

GNU testsuite comparison:

Note: The gnu test tests/id/smack was skipped on 'main' but is now failing.
Note: The gnu test tests/mkdir/smack-no-root was skipped on 'main' but is now failing.
Note: The gnu test tests/mkdir/smack-root was skipped on 'main' but is now failing.

@github-actions
Copy link

GNU testsuite comparison:

Note: The gnu test tests/basenc/bounded-memory is now being skipped but was previously passing.

@mattsu2020 mattsu2020 marked this pull request as draft December 29, 2025 04:59
@github-actions
Copy link

GNU testsuite comparison:

Note: The gnu test tests/id/smack was skipped on 'main' but is now failing.
Note: The gnu test tests/mkdir/smack-no-root was skipped on 'main' but is now failing.
Note: The gnu test tests/mkdir/smack-root was skipped on 'main' but is now failing.

@github-actions
Copy link

GNU testsuite comparison:

GNU test failed: tests/tty/tty-eof. tests/tty/tty-eof is passing on 'main'. Maybe you have to rebase?
Congrats! The gnu test tests/misc/close-stdout is no longer failing!

@github-actions
Copy link

GNU testsuite comparison:

Congrats! The gnu test tests/misc/close-stdout is no longer failing!

@oech3
Copy link
Contributor

oech3 commented Dec 29, 2025

I don't hope to fixing everyng to keep size of PR small, but is it related with #9769 #9795?

@mattsu2020
Copy link
Contributor Author

I don't hole to fixing everyng to keep size of PR small, but is it related with #9769 #9795?

#9769
#9795
Both issues need to be fixed separately.

@mattsu2020 mattsu2020 marked this pull request as ready for review December 29, 2025 09:16
@oech3
Copy link
Contributor

oech3 commented Dec 29, 2025

OK. Even so, the function you added to uucore looks helpful for them.

/// This function handles non-UTF-8 environment variable names and values correctly by using
/// raw bytes on Unix systems.
pub fn print_all_env_vars<T: fmt::Display>(line_ending: T) -> io::Result<()> {
crate::check_stdout_write(1)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

please add a comment explaining why this line
it isn't obvious

Copy link
Contributor

Choose a reason for hiding this comment

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

the comment doesn't help me to understand why you are adding this here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At startup

stdout_is_closed() becomes true, so stdout_was_closed is true.
During output

print_all_env_vars repeatedly calls stdout.write_all_os(...).
This uses OsWrite, so it does not go through TrackingWriter.
Without check_stdout_write, stdout_was_written is never set.
During final flush

stdout.flush() may fail with EBADF, etc.
If stdout_was_written == false, it takes the branch that ignores flush errors for closed stdout.
Result

Even though output was attempted and failed,
the error is silently ignored, and it can exit with code 0.

In cases like the following, exit 0 may occur.
( exec 1>&-; env -i FOO=bar ./target/debug/printenv -0 ); echo "exit=$?"

@github-actions
Copy link

GNU testsuite comparison:

Congrats! The gnu test tests/misc/close-stdout is no longer failing!

@oech3

This comment was marked as resolved.

@github-actions
Copy link

github-actions bot commented Jan 5, 2026

GNU testsuite comparison:

Skip an intermittent issue tests/timeout/timeout (fails in this run but passes in the 'main' branch)

@mattsu2020 mattsu2020 requested a review from sylvestre January 5, 2026 10:16
Copy link
Contributor

@sylvestre sylvestre left a comment

Choose a reason for hiding this comment

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

sorry but i need more time to think about it
i am not convinced by the changes src/uucore/src/lib/lib.rs

@mattsu2020 mattsu2020 marked this pull request as draft January 14, 2026 11:15
@mattsu2020 mattsu2020 force-pushed the misc/close-stdout.sh branch from aa90754 to 5513284 Compare January 14, 2026 11:34
@codspeed-hq
Copy link

codspeed-hq bot commented Jan 15, 2026

Merging this PR will not alter performance

✅ 142 untouched benchmarks
⏩ 180 skipped benchmarks1


Comparing mattsu2020:misc/close-stdout.sh (667a10a) with main (5ccce19)

Open in CodSpeed

Footnotes

  1. 180 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.

@github-actions
Copy link

GNU testsuite comparison:

Note: The gnu test tests/basenc/bounded-memory is now being skipped but was previously passing.

…r handling

Introduce a new `write_stdout_line` function that locks stdout and returns a Result to handle write failures gracefully. Replace all `println!` calls in verbose output, debug messages, and path printing with this function, propagating errors via `CopyResult<()>` instead of panicking on I/O errors. This improves reliability when stdout is closed or encounters issues during cp operations.
…justing exit code

- Made `code` mutable to allow modification on flush errors.
- Ignore BrokenPipe errors during stdout flush to avoid breaking utilities that silence it (e.g., seq).
- Set exit code to 1 if a flush error occurs and the original code was 0, treating write errors as failures.
- Added check to capture if stdout was closed before utility execution using fcntl on Unix.
- Modified flush error handling to ignore EBADF errors when stdout was pre-closed, avoiding false failures for silent commands.
Reorder imports for consistency and refactor the ignore_closed_stdout condition to a more concise form, improving code readability without altering functionality.
Add `stdout_is_closed()` and `is_closed_stdout_error()` functions to check if stdout is closed and detect write errors due to closed stdout. Refactor the `bin!` macro to use these functions, reducing code duplication and improving reusability across utilities.
Implement mechanism to detect if stdout was closed at process start and track write attempts. Add `check_stdout_write()` to error on writes when stdout is closed, preventing issues in utilities like cp and printf. Use `TrackingWriter` in printf for write monitoring. This improves robustness in scenarios like pipeline redirections or closed stdout.
…lity

- Condensed multi-line cfg_attr into a single line in early_stdout_state mod
- Split long variable assignment across lines in bin macro for better readability

This improves code consistency and readability without changing functionality.
Add a check to ensure stdout is writable before printing environment variables, preventing errors when stdout is closed. This uses a non-zero sentinel to trigger the check and mark write attempt.
- Updated the comment to better explain that the path writes via OsWrite, bypassing TrackingWriter, thus requiring an early check to mark stdout as "written" and fail if fd 1 is already closed. This improves code readability and understanding of the stdout-closed handling logic.
Implement atomic-based stdout state management in cp and printf utilities to detect if stdout was closed before writing, avoiding errors in broken pipe scenarios (e.g., when piped output is terminated early). Added libc dependency for Unix-specific fcntl checks, ensuring proper error handling on platforms where stdout closure can occur.
Add 'devnull' to the spell-checker ignore comments in src/uu/cp/src/cp.rs and src/uu/printf/src/printf.rs to prevent false positives, as 'devnull' refers to the /dev/null device and is a valid technical term in the codebase.
Update various crates in fuzz/Cargo.lock to their latest versions, including blake2b_simd, blake3, cc, clap_lex, constant_time_eq, crc-fast, data-encoding, and others for improved performance, security fixes, and compatibility.
… readability

- Split arguments onto multiple lines in both cp.rs and printf.rs to improve code formatting and consistency. No functional changes.
@mattsu2020 mattsu2020 force-pushed the misc/close-stdout.sh branch from 6c6acf3 to bf227bd Compare January 25, 2026 10:01
@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/tail/inotify-dir-recreate (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/tty/tty-eof (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/tail/retry is no longer failing!

mattsu2020 and others added 2 commits January 26, 2026 22:01
Add tests to verify that `cp` and `printf` utilities handle stdout failures correctly. Specifically, test cases are added to check behavior when stdout is closed or redirected to `/dev/full`. These tests ensure that the utilities fail gracefully and provide appropriate error messages when stdout operations cannot be completed.
@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/tty/tty-eof (fails in this run but passes in the 'main' branch)

@ChrisDryden
Copy link
Collaborator

We now already have the ability to read the state of the stdin stdout and stderr and its called at the beginning of all of the utilities, we do not have to create that behavior again in a new library.

I think the majority of the handling of these gnu test cases can be handled in the error handling library with the already existing. This would require us to swap println with writeln and add the error handler ? but it wouldn't require adding a whole new library for writing output. Heres an example branch that shows that it can pass the gnu tests with just the change from println to writeln: https://github.com/ChrisDryden/coreutils/tree/fix-cp-stdout-write-errors

@mattsu2020 mattsu2020 closed this Jan 26, 2026
@mattsu2020 mattsu2020 deleted the misc/close-stdout.sh branch January 26, 2026 13:44
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.

4 participants