Skip to content

Commit 42d4fe6

Browse files
ppennaesaurezCopilot
authored
pal: query Unix socket peer credentials (#4591)
A service on a local Unix socket may need to authenticate a client against the account running the service. PAL currently has no common way to query either the process's effective user ID or the credentials attached to a connected peer. ## Summary - add `pal::unix::effective_user_id` for ownership and identity checks - add Linux-only `unix_socket_peer_user_id` using `SO_PEERCRED` - accept borrowed socket descriptors so credential lookup does not affect descriptor ownership - reject a credential result whose length does not match `struct ucred`, or whose user ID is the all-ones value reported for a socket without peer credentials - document that authentication needs a connected socket, since a listening socket reports its own process's credentials, and that the result is a snapshot rather than the peer's current identity ## Validation At 37623b0, on Windows x86-64 and Linux x86-64 (Ubuntu 26.04 under WSL2): - `cargo check --locked -p pal` - `cargo clippy --locked --all-targets -p pal -- -D warnings` - `RUSTDOCFLAGS="-D warnings" cargo doc --locked --no-deps -p pal` - `cargo nextest run --locked --profile agent -p pal` - Windows: 33 passed - Linux: 11 passed Also ran: - `cargo xtask fmt --fix` - `cargo check --locked --target aarch64-apple-darwin -p pal`, at f5960c1; later changes touch only Linux code --------- Co-authored-by: Enrique Saurez <enrique.saurez@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ef640993-0003-4e49-b1ed-1b8b4776d0ef
1 parent ecb1380 commit 42d4fe6

3 files changed

Lines changed: 118 additions & 0 deletions

File tree

‎support/pal/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ fs-err.workspace = true
9393
thiserror.workspace = true
9494
tracing.workspace = true
9595

96+
[target.'cfg(target_os = "linux")'.dev-dependencies]
97+
test_with_tracing.workspace = true
98+
9699
[target.'cfg(windows)'.dev-dependencies]
97100
getrandom.workspace = true
98101
test_with_tracing.workspace = true

‎support/pal/src/unix.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
#![expect(unsafe_code)]
77

88
pub mod affinity;
9+
pub mod credentials;
910
pub mod fs;
1011
pub mod pipe;
1112
pub mod process;
1213
pub mod pthread;
1314

15+
pub use credentials::effective_user_id;
1416
use std::fs::File;
1517
use std::io;
1618
use std::io::Error;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! Credentials of the current process and of Unix-socket peers.
5+
6+
#[cfg(target_os = "linux")]
7+
use std::io;
8+
#[cfg(target_os = "linux")]
9+
use std::io::Error;
10+
#[cfg(target_os = "linux")]
11+
use std::os::unix::prelude::*;
12+
13+
/// Returns the effective user ID of the current process.
14+
pub fn effective_user_id() -> u32 {
15+
// SAFETY: geteuid has no preconditions.
16+
unsafe { libc::geteuid() }
17+
}
18+
19+
/// Returns the effective user ID of the peer of a connected Unix socket.
20+
///
21+
/// The kernel captures the peer's credentials once: for a socket returned by
22+
/// `accept(2)`, those the client had when it called `connect(2)`; for a socket
23+
/// that connected to a listener, those the listener had when it called
24+
/// `listen(2)`; and for either socket of a `socketpair(2)` pair, those of the
25+
/// process that created the pair. The result is therefore a snapshot, not
26+
/// necessarily the peer's current identity: it does not change when the peer
27+
/// later changes its credentials or passes its descriptor to another process.
28+
///
29+
/// Authenticate a client only with a connected socket. A listening socket
30+
/// reports the credentials of the process that called `listen(2)`, so this
31+
/// function succeeds for it without identifying any client. It fails with
32+
/// [`io::ErrorKind::InvalidData`] for a socket that records no peer
33+
/// credentials, such as one that is neither connected nor listening, or a
34+
/// datagram socket connected with `connect(2)`.
35+
#[cfg(target_os = "linux")]
36+
pub fn unix_socket_peer_user_id(socket: BorrowedFd<'_>) -> io::Result<u32> {
37+
let mut credentials = libc::ucred {
38+
pid: 0,
39+
uid: 0,
40+
gid: 0,
41+
};
42+
let mut length = size_of::<libc::ucred>() as libc::socklen_t;
43+
// SAFETY: `socket` is valid for the call and the output pointers refer to
44+
// writable objects with the supplied sizes.
45+
let result = unsafe {
46+
libc::getsockopt(
47+
socket.as_raw_fd(),
48+
libc::SOL_SOCKET,
49+
libc::SO_PEERCRED,
50+
std::ptr::from_mut(&mut credentials).cast(),
51+
&mut length,
52+
)
53+
};
54+
if result != 0 {
55+
return Err(Error::last_os_error());
56+
}
57+
if length as usize != size_of::<libc::ucred>() {
58+
return Err(Error::new(
59+
io::ErrorKind::InvalidData,
60+
"SO_PEERCRED returned an invalid credential length",
61+
));
62+
}
63+
if credentials.uid == libc::uid_t::MAX {
64+
return Err(Error::new(
65+
io::ErrorKind::InvalidData,
66+
"SO_PEERCRED returned no peer user ID",
67+
));
68+
}
69+
Ok(credentials.uid)
70+
}
71+
72+
#[cfg(all(test, target_os = "linux"))]
73+
mod tests {
74+
use super::*;
75+
use std::io::Read;
76+
use std::io::Write;
77+
use std::os::linux::net::SocketAddrExt;
78+
use std::os::unix::net::SocketAddr;
79+
use std::os::unix::net::UnixDatagram;
80+
use std::os::unix::net::UnixStream;
81+
use test_with_tracing::test;
82+
83+
#[test]
84+
fn stream_pair_peers_report_the_effective_user_id() {
85+
let (mut left, mut right) = UnixStream::pair().unwrap();
86+
let user_id = effective_user_id();
87+
assert_eq!(unix_socket_peer_user_id(left.as_fd()).unwrap(), user_id);
88+
assert_eq!(unix_socket_peer_user_id(right.as_fd()).unwrap(), user_id);
89+
90+
// The lookups only borrow the descriptors, so both ends still work.
91+
let mut buffer = [0; 4];
92+
left.write_all(b"ping").unwrap();
93+
right.read_exact(&mut buffer).unwrap();
94+
assert_eq!(&buffer, b"ping");
95+
right.write_all(b"pong").unwrap();
96+
left.read_exact(&mut buffer).unwrap();
97+
assert_eq!(&buffer, b"pong");
98+
}
99+
100+
#[test]
101+
fn connected_datagram_without_peer_credentials_is_rejected() {
102+
// Use an abstract address so that the test does not depend on the
103+
// filesystem or on the length limit of Unix socket pathnames.
104+
let name = format!("pal-peer-credentials-{}", std::process::id());
105+
let address = SocketAddr::from_abstract_name(name).unwrap();
106+
let _server = UnixDatagram::bind_addr(&address).unwrap();
107+
let client = UnixDatagram::unbound().unwrap();
108+
client.connect_addr(&address).unwrap();
109+
110+
let error = unix_socket_peer_user_id(client.as_fd()).unwrap_err();
111+
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
112+
}
113+
}

0 commit comments

Comments
 (0)