tr: Fix regression causing read error with sockets (#8083)

* tr: Fix regression causing read error with sockets

The test used for determining if a file descriptor
tested with fstat points to a directory is traditionally
done by using the S_IFMT mask[^1][^2], e.g.:
    #define S_ISDIR(m)	(((m) & S_IFMT) == S_IFDIR)
In Rust, this translates to 'm & libc::S_IFMT == libc::S_IFDIR'.
is_stdin_directory() uses 'has!(mode, S_IFDIR)', which is
**not** equivalent and causes non-directory sockets to be
incorrectly recognized as directories. This causes uu-tr
to break when used with all sockets created with socketpair,
including ksh93 pipes[^3]. Below is an example Rust program
demonstrating why the current check is bogus:
  fn main() {
     use nix::sys::socket::{socketpair, SockFlag, AddressFamily, SockType};
     use nix::sys::stat::fstat;
     use libc::{S_IFDIR, S_IFMT, mode_t};
     let (_fd1, fd2) = socketpair(AddressFamily::Unix, SockType::Stream, None, SockFlag::empty()).unwrap();
     let mode = fstat(&fd2).unwrap().st_mode as mode_t;
     if mode & S_IFMT == S_IFDIR {   // Equivalent to S_ISDIR()
         println!("Not reached");    // Not a dir, so unreachable
     }
     if mode & S_IFDIR == S_IFDIR {  // Bogus check for S_IFDIR
         println!("BAD");
     }
  }

- is_stdin_directory(): Fix the regression introduced in 3e4221a4
  by replacing the bogus check with one equivalent to S_ISDIR().
- test_tr.rs: Add a regression test for this bug.

[^1]: https://sourceware.org/git/?p=glibc.git;a=blob;f=io/sys/stat.h;h=4bea9e9a#l123
[^2]: https://git.musl-libc.org/cgit/musl/tree/include/sys/stat.h?id=047a1639#n51
[^3]: cc5e0692/src/cmd/ksh93/sh/io.c (L98-L102)

Fixes https://github.com/uutils/coreutils/issues/7658

* Add comment explaining rationale

Also fix cargo clippy lint.
(In case it isn't obvious, I'm fairly new to Rust)

* Fix more lint
This commit is contained in:
Johnothan King 2025-07-09 06:47:16 -07:00 committed by GitHub
parent 1d85566779
commit 6d30b9110a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 43 additions and 2 deletions

View file

@ -154,6 +154,7 @@ IFSOCK
IRGRP
IROTH
IRUSR
ISDIR
ISGID
ISUID
ISVTX
@ -205,6 +206,7 @@ setgid
setgroups
settime
setuid
socketpair
socktype
statfs
statp

10
Cargo.lock generated
View file

@ -1692,6 +1692,15 @@ dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -1729,6 +1738,7 @@ dependencies = [
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
]
[[package]]

View file

@ -537,7 +537,13 @@ hex-literal = "1.0.0"
rstest.workspace = true
[target.'cfg(unix)'.dev-dependencies]
nix = { workspace = true, features = ["process", "signal", "user", "term"] }
nix = { workspace = true, features = [
"process",
"signal",
"socket",
"user",
"term",
] }
rlimit = "0.10.1"
xattr.workspace = true

View file

@ -720,7 +720,9 @@ pub fn is_stdin_directory(stdin: &Stdin) -> bool {
{
use nix::sys::stat::fstat;
let mode = fstat(stdin.as_fd()).unwrap().st_mode as mode_t;
has!(mode, S_IFDIR)
// We use the S_IFMT mask ala S_ISDIR() to avoid mistaking
// sockets for directories.
mode & S_IFMT == S_IFDIR
}
#[cfg(windows)]

View file

@ -1563,3 +1563,24 @@ fn test_broken_pipe_no_error() {
.run_stdout_starts_with(b"")
.fails_silently();
}
#[cfg(not(windows))]
#[test]
fn test_stdin_is_socket() {
use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair};
use nix::unistd::write;
let (fd1, fd2) = socketpair(
AddressFamily::Unix,
SockType::Stream,
None,
SockFlag::empty(),
)
.unwrap();
write(fd1, b"::").unwrap();
new_ucmd!()
.args(&[":", ";"])
.set_stdin(fd2)
.succeeds()
.stdout_is(";;");
}