Skip to content

Commit b0004a0

Browse files
committed
feat(uucore): add stdout write checking and tracking
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.
1 parent 906f53d commit b0004a0

File tree

4 files changed

+126
-13
lines changed

4 files changed

+126
-13
lines changed

src/uu/cp/src/cp.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ pub type CopyResult<T> = Result<T, CpError>;
139139
fn write_stdout_line(args: fmt::Arguments) -> CopyResult<()> {
140140
use std::io::Write;
141141

142+
uucore::check_stdout_write(1)?;
142143
let mut stdout = io::stdout().lock();
143144
stdout.write_fmt(args)?;
144145
stdout.write_all(b"\n")?;

src/uu/printf/src/printf.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
3838

3939
let mut format_seen = false;
4040
// Parse and process the format string
41+
let mut stdout = uucore::TrackingWriter::new(stdout());
4142
let mut args = FormatArguments::new(&values);
4243
for item in parse_spec_and_escape(format) {
4344
if let Ok(FormatItem::Spec(_)) = item {
4445
format_seen = true;
4546
}
46-
match item?.write(stdout(), &mut args)? {
47+
match item?.write(&mut stdout, &mut args)? {
4748
ControlFlow::Continue(()) => {}
4849
ControlFlow::Break(()) => return Ok(()),
4950
}
@@ -70,7 +71,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
7071

7172
while !args.is_exhausted() {
7273
for item in parse_spec_and_escape(format) {
73-
match item?.write(stdout(), &mut args)? {
74+
match item?.write(&mut stdout, &mut args)? {
7475
ControlFlow::Continue(()) => {}
7576
ControlFlow::Break(()) => return Ok(()),
7677
}

src/uucore/src/lib/lib.rs

Lines changed: 117 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@ use std::iter;
144144
use std::os::unix::ffi::{OsStrExt, OsStringExt};
145145
use std::str;
146146
use std::str::Utf8Chunk;
147-
use std::sync::{LazyLock, atomic::Ordering};
147+
use std::sync::{
148+
LazyLock,
149+
atomic::{AtomicBool, Ordering},
150+
};
148151

149152
/// Disables the custom signal handlers installed by Rust for stack-overflow handling. With those custom signal handlers processes ignore the first SIGBUS and SIGSEGV signal they receive.
150153
/// See <https://github.com/rust-lang/rust/blob/8ac1525e091d3db28e67adcbbd6db1e1deaa37fb/src/libstd/sys/unix/stack_overflow.rs#L71-L92> for details.
@@ -169,19 +172,118 @@ pub fn disable_rust_signal_handlers() -> Result<(), Errno> {
169172
pub fn stdout_is_closed() -> bool {
170173
#[cfg(unix)]
171174
{
172-
use nix::fcntl::{FcntlArg, fcntl};
173-
match fcntl(std::io::stdout(), FcntlArg::F_GETFL) {
174-
Ok(_) => false,
175-
Err(nix::errno::Errno::EBADF) => true,
176-
Err(_) => false,
175+
let res = unsafe { nix::libc::fcntl(nix::libc::STDOUT_FILENO, nix::libc::F_GETFL) };
176+
if res != -1 {
177+
return false;
177178
}
179+
matches!(nix::errno::Errno::last(), nix::errno::Errno::EBADF)
178180
}
179181
#[cfg(not(unix))]
180182
{
181183
false
182184
}
183185
}
184186

187+
static STDOUT_WRITTEN: AtomicBool = AtomicBool::new(false);
188+
static STDOUT_WAS_CLOSED: AtomicBool = AtomicBool::new(false);
189+
static STDOUT_WAS_CLOSED_SET: AtomicBool = AtomicBool::new(false);
190+
191+
/// Reset the stdout-written flag for the current process.
192+
pub fn reset_stdout_written() {
193+
STDOUT_WRITTEN.store(false, Ordering::Relaxed);
194+
}
195+
196+
/// Record whether stdout was closed at process start.
197+
pub fn set_stdout_was_closed(value: bool) {
198+
STDOUT_WAS_CLOSED.store(value, Ordering::Relaxed);
199+
STDOUT_WAS_CLOSED_SET.store(true, Ordering::Relaxed);
200+
}
201+
202+
/// Returns true if stdout was closed at process start.
203+
pub fn stdout_was_closed() -> bool {
204+
STDOUT_WAS_CLOSED.load(Ordering::Relaxed)
205+
}
206+
207+
/// Initialize stdout state if it wasn't set by early startup code.
208+
pub fn init_stdout_state() {
209+
if !STDOUT_WAS_CLOSED_SET.load(Ordering::Relaxed) {
210+
set_stdout_was_closed(stdout_is_closed());
211+
}
212+
}
213+
214+
/// Mark that at least one non-empty write to stdout was attempted.
215+
pub fn mark_stdout_written() {
216+
STDOUT_WRITTEN.store(true, Ordering::Relaxed);
217+
}
218+
219+
/// Returns true if any write to stdout was attempted.
220+
pub fn stdout_was_written() -> bool {
221+
STDOUT_WRITTEN.load(Ordering::Relaxed)
222+
}
223+
224+
#[cfg(unix)]
225+
mod early_stdout_state {
226+
use super::set_stdout_was_closed;
227+
228+
extern "C" fn init() {
229+
set_stdout_was_closed(super::stdout_is_closed());
230+
}
231+
232+
#[used]
233+
#[cfg_attr(
234+
target_os = "macos",
235+
unsafe(link_section = "__DATA,__mod_init_func")
236+
)]
237+
#[cfg_attr(not(target_os = "macos"), unsafe(link_section = ".init_array"))]
238+
static INIT: extern "C" fn() = init;
239+
}
240+
241+
/// Record a pending stdout write and error if stdout was closed at startup.
242+
pub fn check_stdout_write(len: usize) -> std::io::Result<()> {
243+
if len == 0 {
244+
return Ok(());
245+
}
246+
mark_stdout_written();
247+
if stdout_was_closed() {
248+
#[cfg(unix)]
249+
{
250+
return Err(std::io::Error::from_raw_os_error(
251+
nix::errno::Errno::EBADF as i32,
252+
));
253+
}
254+
#[cfg(not(unix))]
255+
{
256+
return Err(std::io::Error::new(
257+
std::io::ErrorKind::BrokenPipe,
258+
"stdout was closed",
259+
));
260+
}
261+
}
262+
Ok(())
263+
}
264+
265+
/// A writer wrapper that marks stdout as written when non-empty data is written.
266+
pub struct TrackingWriter<W> {
267+
inner: W,
268+
}
269+
270+
impl<W> TrackingWriter<W> {
271+
pub fn new(inner: W) -> Self {
272+
Self { inner }
273+
}
274+
}
275+
276+
impl<W: std::io::Write> std::io::Write for TrackingWriter<W> {
277+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
278+
check_stdout_write(buf.len())?;
279+
self.inner.write(buf)
280+
}
281+
282+
fn flush(&mut self) -> std::io::Result<()> {
283+
self.inner.flush()
284+
}
285+
}
286+
185287
/// Returns true if the error corresponds to writing to a closed stdout.
186288
pub fn is_closed_stdout_error(err: &std::io::Error, stdout_was_closed: bool) -> bool {
187289
if !stdout_was_closed {
@@ -228,6 +330,12 @@ macro_rules! bin {
228330
use std::io::Write;
229331
use uucore::locale;
230332

333+
// Capture whether stdout was already closed before we run the utility.
334+
// This must happen before any setup that might open files and reuse fd 1.
335+
uucore::init_stdout_state();
336+
uucore::reset_stdout_written();
337+
let stdout_was_closed = uucore::stdout_was_closed();
338+
231339
// Preserve inherited SIGPIPE settings (e.g., from env --default-signal=PIPE)
232340
uucore::panic::preserve_inherited_sigpipe();
233341

@@ -245,17 +353,15 @@ macro_rules! bin {
245353
std::process::exit(99)
246354
});
247355

248-
// Capture whether stdout was already closed before we run the utility.
249-
// This avoids treating a closed stdout as a flush failure for "silent" commands.
250-
let stdout_was_closed = uucore::stdout_is_closed();
251-
252356
// execute utility code
253357
let mut code = $util::uumain(uucore::args_os());
254358
// (defensively) flush stdout for utility prior to exit; see <https://github.com/rust-lang/rust/issues/23818>
255359
if let Err(e) = std::io::stdout().flush() {
256360
// Treat write errors as a failure, but ignore BrokenPipe to avoid
257361
// breaking utilities that intentionally silence it (e.g., seq).
258-
let ignore_closed_stdout = uucore::is_closed_stdout_error(&e, stdout_was_closed);
362+
let stdout_was_written = uucore::stdout_was_written();
363+
let ignore_closed_stdout = uucore::is_closed_stdout_error(&e, stdout_was_closed)
364+
&& !stdout_was_written;
259365
if e.kind() != std::io::ErrorKind::BrokenPipe && !ignore_closed_stdout {
260366
eprintln!("Error flushing stdout: {e}");
261367
if code == 0 {

src/uucore/src/lib/mods/display.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub use os_display::{Quotable, Quoted};
4949
/// using low-level library calls and bypassing `io::Write`. This is not a big priority
5050
/// because broken filenames are much rarer on Windows than on Unix.
5151
pub fn println_verbatim<S: AsRef<OsStr>>(text: S) -> io::Result<()> {
52+
crate::check_stdout_write(1)?;
5253
let mut stdout = io::stdout().lock();
5354
stdout.write_all_os(text.as_ref())?;
5455
stdout.write_all(b"\n")?;
@@ -57,6 +58,9 @@ pub fn println_verbatim<S: AsRef<OsStr>>(text: S) -> io::Result<()> {
5758

5859
/// Like `println_verbatim`, without the trailing newline.
5960
pub fn print_verbatim<S: AsRef<OsStr>>(text: S) -> io::Result<()> {
61+
if !text.as_ref().is_empty() {
62+
crate::check_stdout_write(1)?;
63+
}
6064
io::stdout().write_all_os(text.as_ref())
6165
}
6266

@@ -125,6 +129,7 @@ impl OsWrite for Box<dyn OsWrite> {
125129
/// This function handles non-UTF-8 environment variable names and values correctly by using
126130
/// raw bytes on Unix systems.
127131
pub fn print_all_env_vars<T: fmt::Display>(line_ending: T) -> io::Result<()> {
132+
crate::check_stdout_write(1)?;
128133
let mut stdout = io::stdout().lock();
129134
for (name, value) in env::vars_os() {
130135
stdout.write_all_os(&name)?;

0 commit comments

Comments
 (0)