1#![deny(unsafe_code)]
2
3#[cfg(target_os = "linux")]
4mod seam;
5
6use std::fmt;
7use std::sync::{Arc, Mutex, PoisonError};
8
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub enum Refusal {
11 Absent(&'static str),
12 Bad(String),
13 Errno { word: &'static str, errno: i32 },
14}
15
16impl fmt::Display for Refusal {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Refusal::Absent(word) => write!(
20 f,
21 "{word}: this host's kernel has no such call; the kernel surface runs on Linux"
22 ),
23 Refusal::Bad(why) => f.write_str(why),
24 Refusal::Errno { word, errno } => {
25 let text = std::io::Error::from_raw_os_error(*errno);
26 write!(f, "{word}: {text}")
27 }
28 }
29 }
30}
31
32#[derive(Debug)]
33pub struct Fd {
34 raw: Mutex<Option<i32>>,
35}
36
37impl Fd {
38 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
39 fn owning(raw: i32) -> Self {
40 Self {
41 raw: Mutex::new(Some(raw)),
42 }
43 }
44
45 #[must_use]
46 pub fn number(&self) -> Option<i32> {
47 *self.raw.lock().unwrap_or_else(PoisonError::into_inner)
48 }
49
50 pub fn close(&self) {
51 let taken = self
52 .raw
53 .lock()
54 .unwrap_or_else(PoisonError::into_inner)
55 .take();
56 #[cfg(target_os = "linux")]
57 if let Some(raw) = taken {
58 seam::close(raw);
59 }
60 #[cfg(not(target_os = "linux"))]
61 let _ = taken;
62 }
63
64 fn live(&self, word: &str) -> Result<i32, Refusal> {
65 self.number()
66 .ok_or_else(|| Refusal::Bad(format!("{word}: this file descriptor is closed")))
67 }
68}
69
70impl Drop for Fd {
71 fn drop(&mut self) {
72 self.close();
73 }
74}
75
76#[derive(Clone, Debug)]
77pub enum Slot {
78 Int { at: usize, width: usize, value: u64 },
79 Bytes { at: usize, bytes: Vec<u8> },
80 In { at: usize, bytes: Vec<u8> },
81 Out { at: usize, capacity: usize },
82 InOut { at: usize, bytes: Vec<u8> },
83 Strings { at: usize, strings: Vec<String> },
84 Descriptor { at: usize, fd: Arc<Fd> },
85}
86
87#[derive(Debug)]
88pub struct Marshalled {
89 pub attr: Vec<u8>,
90 in_buffers: Vec<Vec<u8>>,
91 out_buffers: Vec<Vec<u8>>,
92}
93
94impl Marshalled {
95 #[must_use]
96 pub fn out_buffers(&self) -> &[Vec<u8>] {
97 &self.out_buffers
98 }
99
100 #[must_use]
101 pub fn in_buffers(&self) -> &[Vec<u8>] {
102 &self.in_buffers
103 }
104}
105
106fn put(attr: &mut [u8], at: usize, bytes: &[u8], word: &str) -> Result<(), Refusal> {
107 let end = at
108 .checked_add(bytes.len())
109 .filter(|end| *end <= attr.len())
110 .ok_or_else(|| {
111 Refusal::Bad(format!(
112 "{word}: a field of {} bytes at {at} does not fit a {}-byte struct",
113 bytes.len(),
114 attr.len()
115 ))
116 })?;
117 attr[at..end].copy_from_slice(bytes);
118 Ok(())
119}
120
121fn address(buffer: &mut [u8]) -> [u8; 8] {
122 let raw = if buffer.is_empty() {
123 0
124 } else {
125 buffer.as_mut_ptr() as usize as u64
126 };
127 raw.to_ne_bytes()
128}
129
130pub fn marshal(word: &str, size: usize, slots: &[Slot]) -> Result<Marshalled, Refusal> {
131 let mut attr = vec![0u8; size];
132 let mut in_buffers: Vec<Vec<u8>> = Vec::new();
133 let mut out_buffers: Vec<Vec<u8>> = Vec::new();
134 for slot in slots {
135 match slot {
136 Slot::Int { at, width, value } => {
137 let bytes = value.to_ne_bytes();
138 if !matches!(width, 1 | 2 | 4 | 8) {
139 return Err(Refusal::Bad(format!(
140 "{word}: an integer field is 1, 2, 4 or 8 bytes, got {width}"
141 )));
142 }
143 #[cfg(target_endian = "little")]
144 let narrow = &bytes[..*width];
145 #[cfg(target_endian = "big")]
146 let narrow = &bytes[8 - *width..];
147 put(&mut attr, *at, narrow, word)?;
148 }
149 Slot::Bytes { at, bytes } => put(&mut attr, *at, bytes, word)?,
150 Slot::In { at, bytes } => {
151 let mut held = bytes.clone();
152 put(&mut attr, *at, &address(&mut held), word)?;
153 in_buffers.push(held);
154 }
155 Slot::Out { at, capacity } => {
156 let mut held = vec![0u8; *capacity];
157 put(&mut attr, *at, &address(&mut held), word)?;
158 out_buffers.push(held);
159 }
160 Slot::InOut { at, bytes } => {
161 let mut held = bytes.clone();
162 put(&mut attr, *at, &address(&mut held), word)?;
163 out_buffers.push(held);
164 }
165 Slot::Strings { at, strings } => {
166 let mut pointers: Vec<u8> = Vec::with_capacity(strings.len() * 8);
167 for text in strings {
168 let mut held = text.clone().into_bytes();
169 held.push(0);
170 pointers.extend_from_slice(&address(&mut held));
171 in_buffers.push(held);
172 }
173 put(&mut attr, *at, &address(&mut pointers), word)?;
174 in_buffers.push(pointers);
175 }
176 Slot::Descriptor { at, fd } => {
177 let raw = fd.live(word)?;
178 put(&mut attr, *at, &raw.to_ne_bytes(), word)?;
179 }
180 }
181 }
182 Ok(Marshalled {
183 attr,
184 in_buffers,
185 out_buffers,
186 })
187}
188
189#[derive(Debug)]
190pub struct Answer {
191 pub ret: i64,
192 pub errno: Option<i32>,
193 pub attr: Vec<u8>,
194 pub out: Vec<Vec<u8>>,
195 pub fd: Option<Fd>,
196}
197
198pub fn bpf(command: u32, size: usize, slots: &[Slot], fd_result: bool) -> Result<Answer, Refusal> {
199 const WORD: &str = "bpf_syscall";
200 let mut m = marshal(WORD, size, slots)?;
201 #[cfg(target_os = "linux")]
202 {
203 let outcome = seam::bpf(command, &mut m.attr);
204 let held = std::mem::take(&mut m.in_buffers);
205 drop(held);
206 let (ret, errno) = match outcome {
207 Ok(n) => (n, None),
208 Err(e) => (-1, Some(e)),
209 };
210 let fd = (fd_result && errno.is_none())
211 .then(|| i32::try_from(ret).ok().map(Fd::owning))
212 .flatten();
213 Ok(Answer {
214 ret,
215 errno,
216 attr: std::mem::take(&mut m.attr),
217 out: std::mem::take(&mut m.out_buffers),
218 fd,
219 })
220 }
221 #[cfg(not(target_os = "linux"))]
222 {
223 let _ = (command, fd_result, &mut m);
224 Err(Refusal::Absent(WORD))
225 }
226}
227
228pub fn perf_event_open(
229 size: usize,
230 slots: &[Slot],
231 pid: i32,
232 cpu: i32,
233 group: Option<&Fd>,
234 flags: u64,
235) -> Result<Fd, Refusal> {
236 const WORD: &str = "perf_event_open";
237 let mut m = marshal(WORD, size, slots)?;
238 #[cfg(target_os = "linux")]
239 {
240 let group = group.map_or(Ok(-1), |g| g.live(WORD))?;
241 let outcome = seam::perf_event_open(&mut m.attr, pid, cpu, group, flags);
242 drop(std::mem::take(&mut m.in_buffers));
243 outcome
244 .map(Fd::owning)
245 .map_err(|errno| Refusal::Errno { word: WORD, errno })
246 }
247 #[cfg(not(target_os = "linux"))]
248 {
249 let _ = (&mut m, pid, cpu, group, flags);
250 Err(Refusal::Absent(WORD))
251 }
252}
253
254pub fn ioctl(fd: &Fd, request: u64, argument: u64) -> Result<i64, Refusal> {
255 const WORD: &str = "fd_ioctl";
256 #[cfg(target_os = "linux")]
257 {
258 seam::ioctl(fd.live(WORD)?, request, argument)
259 .map_err(|errno| Refusal::Errno { word: WORD, errno })
260 }
261 #[cfg(not(target_os = "linux"))]
262 {
263 let _ = (fd, request, argument);
264 Err(Refusal::Absent(WORD))
265 }
266}
267
268#[derive(Debug)]
269pub struct Ring {
270 #[cfg(target_os = "linux")]
271 inner: seam::Ring,
272 #[allow(dead_code)]
273 map: Arc<Fd>,
274}
275
276impl Ring {
277 pub fn open(map: Arc<Fd>, size: usize) -> Result<Self, Refusal> {
278 const WORD: &str = "ringbuf_open";
279 if size == 0 || !size.is_power_of_two() {
280 return Err(Refusal::Bad(format!(
281 "{WORD}: a ring buffer's size is a power of two, got {size}"
282 )));
283 }
284 #[cfg(target_os = "linux")]
285 {
286 let raw = map.live(WORD)?;
287 let inner = seam::Ring::open(raw, size)
288 .map_err(|errno| Refusal::Errno { word: WORD, errno })?;
289 Ok(Self { inner, map })
290 }
291 #[cfg(not(target_os = "linux"))]
292 {
293 let _ = map;
294 Err(Refusal::Absent(WORD))
295 }
296 }
297
298 pub fn next(&self, timeout_ms: i32) -> Result<Option<Vec<u8>>, Refusal> {
299 const WORD: &str = "ringbuf_next";
300 #[cfg(target_os = "linux")]
301 {
302 self.inner
303 .next(timeout_ms)
304 .map_err(|errno| Refusal::Errno { word: WORD, errno })
305 }
306 #[cfg(not(target_os = "linux"))]
307 {
308 let _ = timeout_ms;
309 Err(Refusal::Absent(WORD))
310 }
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn integers_land_at_their_offsets_and_the_rest_stays_zero() {
320 let m = marshal(
321 "t",
322 12,
323 &[
324 Slot::Int {
325 at: 0,
326 width: 4,
327 value: 0x0102_0304,
328 },
329 Slot::Int {
330 at: 8,
331 width: 2,
332 value: 0xABCD,
333 },
334 ],
335 )
336 .unwrap();
337 let mut want = vec![0u8; 12];
338 want[..4].copy_from_slice(&0x0102_0304u32.to_ne_bytes());
339 want[8..10].copy_from_slice(&0xABCDu16.to_ne_bytes());
340 assert_eq!(m.attr, want);
341 }
342
343 #[test]
344 fn an_empty_size_with_no_slots_is_an_empty_struct_and_a_field_past_the_end_is_refused() {
345 assert!(marshal("t", 0, &[]).unwrap().attr.is_empty());
346 let e = marshal(
347 "t",
348 4,
349 &[Slot::Int {
350 at: 2,
351 width: 4,
352 value: 1,
353 }],
354 )
355 .unwrap_err();
356 assert!(e.to_string().contains("does not fit"), "{e}");
357 }
358
359 #[test]
360 fn a_buffer_field_holds_the_address_of_a_buffer_kept_for_the_call() {
361 let m = marshal(
362 "t",
363 16,
364 &[
365 Slot::In {
366 at: 0,
367 bytes: vec![7, 8, 9],
368 },
369 Slot::Out { at: 8, capacity: 5 },
370 ],
371 )
372 .unwrap();
373 let held = m.in_buffers[0].as_ptr() as usize as u64;
374 let out = m.out_buffers[0].as_ptr() as usize as u64;
375 assert_eq!(u64::from_ne_bytes(m.attr[0..8].try_into().unwrap()), held);
376 assert_eq!(u64::from_ne_bytes(m.attr[8..16].try_into().unwrap()), out);
377 assert_eq!(m.in_buffers[0], [7, 8, 9]);
378 assert_eq!(m.out_buffers()[0], [0, 0, 0, 0, 0]);
379 }
380
381 #[test]
382 fn a_strings_field_is_an_array_of_addresses_of_nul_terminated_names() {
383 let m = marshal(
384 "t",
385 8,
386 &[Slot::Strings {
387 at: 0,
388 strings: vec!["ab".into(), "c".into()],
389 }],
390 )
391 .unwrap();
392 assert_eq!(m.in_buffers()[0], [b'a', b'b', 0]);
393 assert_eq!(m.in_buffers()[1], [b'c', 0]);
394 let table = &m.in_buffers()[2];
395 assert_eq!(table.len(), 16);
396 let first = u64::from_ne_bytes(table[..8].try_into().unwrap());
397 assert_eq!(first, m.in_buffers()[0].as_ptr() as usize as u64);
398 let addr = u64::from_ne_bytes(m.attr[..8].try_into().unwrap());
399 assert_eq!(addr, table.as_ptr() as usize as u64);
400 }
401
402 #[test]
403 fn an_empty_buffer_is_a_null_address() {
404 let m = marshal(
405 "t",
406 8,
407 &[Slot::In {
408 at: 0,
409 bytes: vec![],
410 }],
411 )
412 .unwrap();
413 assert_eq!(m.attr, [0u8; 8]);
414 }
415
416 #[test]
417 fn a_descriptor_slot_writes_the_number_and_a_closed_one_is_refused() {
418 let fd = Arc::new(Fd {
419 raw: Mutex::new(Some(41)),
420 });
421 let m = marshal(
422 "t",
423 4,
424 &[Slot::Descriptor {
425 at: 0,
426 fd: fd.clone(),
427 }],
428 )
429 .unwrap();
430 assert_eq!(m.attr, 41i32.to_ne_bytes());
431 *fd.raw.lock().unwrap() = None;
432 let e = marshal("t", 4, &[Slot::Descriptor { at: 0, fd }]).unwrap_err();
433 assert!(e.to_string().contains("closed"), "{e}");
434 }
435
436 #[test]
437 fn a_width_that_is_no_integer_width_is_refused() {
438 let e = marshal(
439 "t",
440 8,
441 &[Slot::Int {
442 at: 0,
443 width: 3,
444 value: 1,
445 }],
446 )
447 .unwrap_err();
448 assert!(e.to_string().contains("1, 2, 4 or 8"), "{e}");
449 }
450
451 #[test]
452 fn a_ring_size_that_is_not_a_power_of_two_is_refused_before_any_call() {
453 let map = Arc::new(Fd {
454 raw: Mutex::new(None),
455 });
456 let e = Ring::open(map, 3).unwrap_err();
457 assert!(e.to_string().contains("power of two"), "{e}");
458 }
459
460 #[cfg(not(target_os = "linux"))]
461 #[test]
462 fn off_linux_every_call_is_a_named_absence() {
463 assert_eq!(
464 bpf(0, 0, &[], false).unwrap_err(),
465 Refusal::Absent("bpf_syscall")
466 );
467 assert_eq!(
468 perf_event_open(8, &[], 0, 0, None, 0).unwrap_err(),
469 Refusal::Absent("perf_event_open")
470 );
471 }
472
473 #[cfg(target_os = "linux")]
474 #[test]
475 fn on_linux_a_bad_command_is_an_errno_never_a_crash() {
476 let a = bpf(9_999, 8, &[], false).unwrap();
477 assert!(a.errno.is_some());
478 assert_eq!(a.ret, -1);
479 }
480}