Skip to content

Commit c94de0c

Browse files
committed
feat: add ancillary data (control message) helpers for sendmsg/recvmsg
Closes #614 Add Unix-only safe wrappers for CMSG_* operations: - `cmsg_space(data_len) -> Option<usize>`: compute control buffer size (returns None when data_len overflows c_uint, fixing a truncation path that would produce an undersized buffer) - `ControlMessage<'a>`: a parsed ancillary data entry (level, type, data) - `ControlMessages<'a>`: iterator over a received control buffer; walks via byte-offset arithmetic and ptr::read_unaligned so no aligned reference to cmsghdr is ever formed (avoids UB on 1-byte-aligned Vec<u8> buffers) - `ControlMessageEncoder<'a>`: builder for outgoing control messages; rejects payloads exceeding c_uint::MAX before calling CMSG_SPACE/CMSG_LEN to prevent buffer overflow from silent truncation Also add a Cross CI job to run tests under QEMU on i686, aarch64, and armv7 Linux — the target families where CMSG_ALIGN factor and cmsg_len width differ from x86_64. Enables SCM_RIGHTS file-descriptor passing without depending on libc directly (tracked by plabayo/rama#781).
1 parent 642df44 commit c94de0c

File tree

4 files changed

+412
-5
lines changed

4 files changed

+412
-5
lines changed

.github/workflows/main.yml

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
os: windows-latest
3939
rust: stable
4040
steps:
41-
- uses: actions/checkout@v4
41+
- uses: actions/checkout@v6
4242
- uses: dtolnay/rust-toolchain@master
4343
with:
4444
toolchain: ${{ matrix.rust }}
@@ -52,7 +52,7 @@ jobs:
5252
name: Rustfmt
5353
runs-on: ubuntu-latest
5454
steps:
55-
- uses: actions/checkout@v4
55+
- uses: actions/checkout@v6
5656
- uses: dtolnay/rust-toolchain@stable
5757
with:
5858
components: rustfmt
@@ -110,7 +110,7 @@ jobs:
110110
- x86_64-unknown-redox
111111
- wasm32-wasip2
112112
steps:
113-
- uses: actions/checkout@v4
113+
- uses: actions/checkout@v6
114114
- uses: dtolnay/rust-toolchain@nightly
115115
with:
116116
components: rust-src
@@ -119,11 +119,44 @@ jobs:
119119
run: cargo hack check -Z build-std=std,panic_abort --feature-powerset --target ${{ matrix.target }}
120120
- name: Check docs
121121
run: RUSTDOCFLAGS="-D warnings --cfg docsrs" cargo doc -Z build-std=std,panic_abort --no-deps --all-features --target ${{ matrix.target }}
122+
Cross:
123+
name: Cross-test (${{ matrix.target }})
124+
runs-on: ubuntu-latest
125+
strategy:
126+
fail-fast: false
127+
matrix:
128+
include:
129+
# 32-bit Linux: size_t=4 → cmsg_len is 4 bytes, CMSG_ALIGN factor=4.
130+
# Exercises a different CMSG_* layout than x86_64 (factor=8).
131+
- target: i686-unknown-linux-gnu
132+
rust: stable
133+
# 64-bit ARM Linux: same CMSG_ALIGN factor as x86_64 but different ABI.
134+
- target: aarch64-unknown-linux-gnu
135+
rust: stable
136+
# 32-bit ARM Linux: like i686 but a distinct architecture.
137+
- target: armv7-unknown-linux-gnueabihf
138+
rust: stable
139+
steps:
140+
- uses: actions/checkout@v6
141+
- uses: dtolnay/rust-toolchain@master
142+
with:
143+
toolchain: ${{ matrix.rust }}
144+
targets: ${{ matrix.target }}
145+
- uses: taiki-e/install-action@cross
146+
- name: Run cmsg tests (cross + QEMU)
147+
run: |
148+
cross test --target ${{ matrix.target }} --all-features -- cmsg
149+
cross test --target ${{ matrix.target }} --all-features -- control_message
150+
- name: Run cmsg tests release (cross + QEMU)
151+
run: |
152+
cross test --target ${{ matrix.target }} --all-features --release -- cmsg
153+
cross test --target ${{ matrix.target }} --all-features --release -- control_message
154+
122155
Clippy:
123156
name: Clippy
124157
runs-on: ubuntu-latest
125158
steps:
126-
- uses: actions/checkout@v4
159+
- uses: actions/checkout@v6
127160
- uses: dtolnay/rust-toolchain@stable
128161
with:
129162
components: clippy
@@ -144,7 +177,7 @@ jobs:
144177
# the README for details: https://github.com/awslabs/cargo-check-external-types
145178
- nightly-2024-06-30
146179
steps:
147-
- uses: actions/checkout@v4
180+
- uses: actions/checkout@v6
148181
- name: Install Rust ${{ matrix.rust }}
149182
uses: dtolnay/rust-toolchain@stable
150183
with:

src/cmsg.rs

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
use std::fmt;
2+
use std::mem;
3+
4+
/// Returns the space required in a control message buffer for a single message
5+
/// with `data_len` bytes of ancillary data.
6+
///
7+
/// Returns `None` if `data_len` does not fit in `libc::c_uint`.
8+
///
9+
/// Corresponds to `CMSG_SPACE(3)`.
10+
pub fn cmsg_space(data_len: usize) -> Option<usize> {
11+
let len = libc::c_uint::try_from(data_len).ok()?;
12+
// SAFETY: pure arithmetic.
13+
usize::try_from(unsafe { libc::CMSG_SPACE(len) }).ok()
14+
}
15+
16+
/// A control message parsed from a `recvmsg(2)` control buffer.
17+
///
18+
/// Returned by [`ControlMessages`].
19+
pub struct ControlMessage<'a> {
20+
cmsg_level: i32,
21+
cmsg_type: i32,
22+
data: &'a [u8],
23+
}
24+
25+
impl<'a> ControlMessage<'a> {
26+
/// Corresponds to `cmsg_level` in `cmsghdr`.
27+
pub fn cmsg_level(&self) -> i32 {
28+
self.cmsg_level
29+
}
30+
31+
/// Corresponds to `cmsg_type` in `cmsghdr`.
32+
pub fn cmsg_type(&self) -> i32 {
33+
self.cmsg_type
34+
}
35+
36+
/// The ancillary data payload.
37+
///
38+
/// Corresponds to the data portion following the `cmsghdr`.
39+
pub fn data(&self) -> &'a [u8] {
40+
self.data
41+
}
42+
}
43+
44+
impl<'a> fmt::Debug for ControlMessage<'a> {
45+
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
46+
"ControlMessage".fmt(fmt)
47+
}
48+
}
49+
50+
/// Iterator over control messages in a `recvmsg(2)` control buffer.
51+
///
52+
/// See [`crate::MsgHdrMut::with_control`] and [`crate::MsgHdrMut::control_len`].
53+
pub struct ControlMessages<'a> {
54+
buf: &'a [u8],
55+
offset: usize,
56+
}
57+
58+
impl<'a> ControlMessages<'a> {
59+
/// Create a new `ControlMessages` from the filled control buffer.
60+
///
61+
/// Pass `&raw_buf[..msg.control_len()]` where `raw_buf` is the slice
62+
/// passed to [`crate::MsgHdrMut::with_control`] before calling `recvmsg(2)`.
63+
pub fn new(buf: &'a [u8]) -> Self {
64+
Self { buf, offset: 0 }
65+
}
66+
}
67+
68+
impl<'a> Iterator for ControlMessages<'a> {
69+
type Item = ControlMessage<'a>;
70+
71+
#[allow(clippy::useless_conversion)]
72+
fn next(&mut self) -> Option<Self::Item> {
73+
let hdr_size = mem::size_of::<libc::cmsghdr>();
74+
// SAFETY: pure arithmetic; gives CMSG_ALIGN(sizeof(cmsghdr)).
75+
let data_offset: usize =
76+
usize::try_from(unsafe { libc::CMSG_LEN(0) }).unwrap_or(usize::MAX);
77+
78+
if self.offset + hdr_size > self.buf.len() {
79+
return None;
80+
}
81+
82+
// SAFETY: range is within `buf`; read_unaligned handles any alignment.
83+
let cmsg: libc::cmsghdr = unsafe {
84+
std::ptr::read_unaligned(self.buf.as_ptr().add(self.offset) as *const libc::cmsghdr)
85+
};
86+
87+
let total_len = usize::try_from(cmsg.cmsg_len).unwrap_or(0);
88+
if total_len < data_offset {
89+
return None;
90+
}
91+
let data_len = total_len - data_offset;
92+
93+
let data_abs_start = self.offset + data_offset;
94+
let data_abs_end = data_abs_start.saturating_add(data_len);
95+
if data_abs_end > self.buf.len() {
96+
return None;
97+
}
98+
99+
let item = ControlMessage {
100+
cmsg_level: cmsg.cmsg_level,
101+
cmsg_type: cmsg.cmsg_type,
102+
data: &self.buf[data_abs_start..data_abs_end],
103+
};
104+
105+
// SAFETY: pure arithmetic; CMSG_SPACE(data_len) == CMSG_ALIGN(total_len).
106+
let advance = match libc::c_uint::try_from(data_len) {
107+
Ok(dl) => usize::try_from(unsafe { libc::CMSG_SPACE(dl) }).unwrap_or(usize::MAX),
108+
Err(_) => return None,
109+
};
110+
self.offset = self.offset.saturating_add(advance);
111+
112+
Some(item)
113+
}
114+
}
115+
116+
impl<'a> fmt::Debug for ControlMessages<'a> {
117+
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
118+
"ControlMessages".fmt(fmt)
119+
}
120+
}
121+
122+
/// Builds a control message buffer for use with `sendmsg(2)`.
123+
///
124+
/// See [`crate::MsgHdr::with_control`] and [`cmsg_space`].
125+
pub struct ControlMessageEncoder<'a> {
126+
buf: &'a mut [u8],
127+
len: usize,
128+
}
129+
130+
impl<'a> ControlMessageEncoder<'a> {
131+
/// Create a new `ControlMessageEncoder` backed by `buf`.
132+
///
133+
/// Zeroes `buf` on creation to ensure padding bytes are clean.
134+
/// Allocate `buf` with the sum of [`cmsg_space`] for each intended message.
135+
pub fn new(buf: &'a mut [u8]) -> Self {
136+
buf.fill(0);
137+
Self { buf, len: 0 }
138+
}
139+
140+
/// Append a control message carrying `data`.
141+
///
142+
/// Returns `Err` if `data` exceeds `c_uint::MAX` or the buffer is too small.
143+
pub fn push(&mut self, cmsg_level: i32, cmsg_type: i32, data: &[u8]) -> std::io::Result<()> {
144+
let data_len_uint = libc::c_uint::try_from(data.len()).map_err(|_| {
145+
std::io::Error::new(
146+
std::io::ErrorKind::InvalidInput,
147+
"ancillary data payload too large (exceeds c_uint::MAX)",
148+
)
149+
})?;
150+
// SAFETY: pure arithmetic.
151+
let space: usize =
152+
usize::try_from(unsafe { libc::CMSG_SPACE(data_len_uint) }).unwrap_or(usize::MAX);
153+
if self.len + space > self.buf.len() {
154+
return Err(std::io::Error::new(
155+
std::io::ErrorKind::InvalidInput,
156+
"control message buffer too small",
157+
));
158+
}
159+
// SAFETY: pure arithmetic.
160+
let cmsg_len = unsafe { libc::CMSG_LEN(data_len_uint) };
161+
unsafe {
162+
// SAFETY: offset is within buf; write_unaligned handles alignment 1.
163+
// Use zeroed() + field assignment to handle platform-specific padding
164+
// (e.g. musl adds __pad1); buf is pre-zeroed but the write must be
165+
// self-contained for correctness.
166+
let cmsg_ptr = self.buf.as_mut_ptr().add(self.len) as *mut libc::cmsghdr;
167+
let mut hdr: libc::cmsghdr = mem::zeroed();
168+
hdr.cmsg_len = cmsg_len as _;
169+
hdr.cmsg_level = cmsg_level;
170+
hdr.cmsg_type = cmsg_type;
171+
std::ptr::write_unaligned(cmsg_ptr, hdr);
172+
// SAFETY: CMSG_DATA gives the correct offset past alignment padding.
173+
let data_ptr = libc::CMSG_DATA(cmsg_ptr);
174+
std::ptr::copy_nonoverlapping(data.as_ptr(), data_ptr, data.len());
175+
}
176+
self.len += space;
177+
Ok(())
178+
}
179+
180+
/// Returns the encoded bytes.
181+
///
182+
/// Corresponds to the slice to pass to [`crate::MsgHdr::with_control`].
183+
pub fn as_bytes(&self) -> &[u8] {
184+
&self.buf[..self.len]
185+
}
186+
187+
/// Returns the number of bytes written.
188+
pub fn len(&self) -> usize {
189+
self.len
190+
}
191+
192+
/// Returns `true` if no control messages have been pushed.
193+
pub fn is_empty(&self) -> bool {
194+
self.len == 0
195+
}
196+
}
197+
198+
impl<'a> fmt::Debug for ControlMessageEncoder<'a> {
199+
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
200+
"ControlMessageEncoder".fmt(fmt)
201+
}
202+
}

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ macro_rules! man_links {
172172
};
173173
}
174174

175+
#[cfg(not(any(target_os = "redox", target_os = "wasi")))]
176+
mod cmsg;
175177
mod sockaddr;
176178
mod socket;
177179
mod sockref;
@@ -188,6 +190,8 @@ compile_error!("Socket2 doesn't support the compile target");
188190

189191
use sys::c_int;
190192

193+
#[cfg(not(any(target_os = "redox", target_os = "wasi")))]
194+
pub use cmsg::{cmsg_space, ControlMessage, ControlMessageEncoder, ControlMessages};
191195
pub use sockaddr::{sa_family_t, socklen_t, SockAddr, SockAddrStorage};
192196
#[cfg(not(any(
193197
target_os = "haiku",

0 commit comments

Comments
 (0)