Skip to content

cat: report EBADF for a write-only stdin - #15953

Draft
eylulseyit wants to merge 1 commit into
uutils:mainfrom
eylulseyit:cat-stdin-ebadf
Draft

eylulseyit wants to merge 1 commit into
uutils:mainfrom
eylulseyit:cat-stdin-ebadf

Conversation

@eylulseyit

@eylulseyit eylulseyit commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

When stdin is open but write-only, reading it fails with EBADF. GNU cat reports that and exits 1; uutils cat printed nothing and exited 0.

cat 0>/dev/null; echo "exit=$?"

The Rust standard library's Stdin treats EBADF as end of file, so the error never reached cat. cat now reads stdin through uucore::io::RawReader on unix and wasi, which needed an AsFd impl for RawReader. Windows is unchanged.

test_nohup_replaced_stdin_is_not_readable ran the system cat, which on ubuntu-26.04 runners is uutils and has this bug, so it failed there. The test now runs the uutils cat that was just built, so it checks this fix and no longer depends on the runner.

Closes #15937

@xtqqczze

Copy link
Copy Markdown
Collaborator

please convert to draft until it is ready

@codspeed

codspeed Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning, 1 note

⚠️ 123 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

ℹ️ 1 benchmark has no flamegraph

The flamegraph wasn't generated: most of the time was spent in runtime internals, which would leave nothing useful to analyze.

See affected benchmarks

✅ 401 untouched benchmarks
⏩ 70 skipped benchmarks1


Comparing eylulseyit:cat-stdin-ebadf (78dbff5) with main (8e267ce)2

Open in CodSpeed

Footnotes

  1. 70 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. ↩

  2. No successful run was found on main (19f6602) during the generation of this report, so 8e267ce was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@eylulseyit
eylulseyit marked this pull request as draft October 28, 2026 11:58
Comment thread src/uu/cat/src/cat.rs Outdated
Comment on lines +386 to +387
// std's Stdin reports EBADF as end of file, so a write-only stdin
// would be read as empty. Read the descriptor directly instead.

This comment was marked as resolved.

Comment thread src/uu/cat/src/cat.rs
// std's Stdin reports EBADF as end of file, so a write-only stdin
// would be read as empty. Read the descriptor directly instead.
#[cfg(any(unix, target_os = "wasi"))]
let stdin = uucore::io::RawReader(stdin);

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.

If I understood correctly, I think buffering doesn't change. std's Stdin buffer is 8 KiB and it is skipped for reads at least that big, and cat already reads 64 KiB (31 KiB with -n). I also counted read syscalls with /proc//io for cat -n, it was 43 reads for 1.3 MB both before and after.

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, I checked them all now. I piped 1.3 MB into cat and counted read syscalls in /proc//io, old build (main) vs this branch:

  • plain cat and -u: 0 reads, it uses splice
  • -A -b -e -E -n -s -t -T -v: 43 reads (31 KiB each)
  • plain cat and -u with >> file so splice is not used: 22 reads (64 KiB each)

the numbers are the same before and after in all cases.

what I did not check: only Linux, I did not test macOS/BSD (no splice there, but they use the same read loop as the >> file case), a terminal as stdin, or timing. I only counted reads.

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. Thankyou!

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 review, your comments helped a lot

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.

tried it like this, for each option:

(for i in 1 2 3; do sleep 1; printf 'hello\nworld'; done) | cat -n | python3 -c 'import os,time; t=time.monotonic()
while b := os.read(0, 65536): print(f"+{time.monotonic()-t:.1f}s", b)'

output for -n, same on main and this branch:

+1.0s b'     1\thello\n     2\tworld'
+2.0s b'hello\n     3\tworld'
+3.0s b'hello\n     4\tworld'

all options (none, -u -A -b -e -E -n -s -t -T -v) print each chunk right away at +1s +2s +3s, same on main and this branch, and same as GNU cat for plain and -n. Is it ok?

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.

Comment thread src/uu/cat/src/cat.rs
}
// std's Stdin reports EBADF as end of file, so a write-only stdin
// would be read as empty. Read the descriptor directly instead.
#[cfg(any(unix, target_os = "wasi"))]

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

Comment thread tests/by-util/test_nohup.rs Outdated
// EOF (GNU opens /dev/null write-only). Since nohup execs the command, the
// exit status is the command's own.
// exit status is the command's own. Run our own cat rather than the one in
// PATH, which may be an older uutils cat that reads such a stdin as empty.

@oech3 oech3 Oct 28, 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.

I don't think this change is needed since portable test is always better.

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 thought the system cat on ubuntu-26.04 runners is uutils, and an older one with this bug. So with the portable version, the nohup test would keep failing there until Ubuntu updates its cat, even after this is merged. Is that ok, or should the test stay failing on those runners for now? I can drop this commit if you prefer the portable test.

@oech3 oech3 Oct 28, 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.

I meant your test is better (portable), but comment can be reverted.

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.

Oh sorry I understand now

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 thought the system cat on ubuntu-26.04 runners is uutils

@oech3 We should really switch to GNU coreutils for all runners.

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 don't think that it is necessary. If our pure-Rust tests are not portable, it is our fault and they should be improved instead of working around at system side.

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.

Also it means we abandon actual people on Ubuntu running our tests.

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 can delete that test update commit for another PR if necessary, if it is a big decision? I did that just for fixing the CI test because my first commit had that MinRustV error. @oech3 @xtqqczze

@oech3 oech3 Oct 28, 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.

Wait maintainer's response.

I'm looking at alternative for cat which is 100% installed on unix or Rust toolchain which fails with nohup cmd. But compiling something for this is overkill.

Comment thread tests/by-util/test_nohup.rs Outdated
// PATH, which may be an older uutils cat that reads such a stdin as empty.
#[test]
#[cfg(unix)]
#[cfg(all(unix, feature = "cat"))]

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.

this is the opposite of portable, we shouldn't be depending on our own utils for unrelated tests

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.

Sorry, I don't understand. Previous test depends on system binary which is not portable.
System cat can be busybox, BSD, vlang-coreutils, etc...
This PR made the test reproducible and our cat is verified at different test.

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.

My point is that using uutils cat from main is neither portable nor reproducible. Its behavior can change independently of the test, meaning the test results may depend on the current version of another utility instead of just the code being tested.

As for system cat being BusyBox, BSD, vlang-coreutils, etc., that's exactly what portability means: the test should work across different implementations, not replace that variation with a dependency on our own utility.

@oech3 oech3 Oct 29, 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.

Why do you 100% believe that system coreutils is not buggy? It is not portable.
This PR adds test case for cat which means we pass the CI if and only if we are using reliable cat.
We can catch false-negative too if we rely on system cat.

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.

Sorry, I’m not sure I understand your point. Are you saying that GNU coreutils are not portable? Could you clarify what you mean by "portable" in this context?

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.

Don't asume that system coreutils is GNU coreutils. Also don't assume that GNU coreutils does not have bug too.

@eylulseyit

Copy link
Copy Markdown
Contributor Author

rebased and dropped the nohup test commit, since #15944 handles that test now.

}
#[cfg(any(unix, target_os = "wasi"))]
impl<T: AsFd> AsFd for RawReader<T> {
fn as_fd(&self) -> BorrowedFd<'_> {

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 don't think RawReader should implement AsFd. Since it's an abstraction over a file descriptor, exposing the underlying file descriptor defeats that abstraction.

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 is actually used at Linux and Android.
Aim of RawReader is removing buffering only. It is not intended to refuse using as fd.

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 added it because cat's InputHandle needs Read + AsFd (for splice and the input == output check), so RawReader didn't fit there without it. As far as I understand, the inner fd is already reachable through the pub field, so I don't think it exposes anything new, but I may be missing something. If you'd prefer not to have it in uucore, I can wrap it in a small type inside cat instead.

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'm fine to add it since it is same with std::io::stdin().as_fd(). We could use stdin.0, but it is difficult to understand.

reading a write-only stdin fails with EBADF, but std's Stdin reads it as end of file. So cat printed nothing and exited with 0. Read stdin with uucore's RawReader instead and added AsFd to RawReader so cat can use it.

Closes uutils#15937

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.

cat, uucore::signals: write-only stdin is read as EOF instead of reporting EBADF

3 participants