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 open_read(path: &str) -> Result<Fd, Refusal> {
255 const WORD: &str = "fd_open";
256 #[cfg(target_os = "linux")]
257 {
258 use std::os::fd::IntoRawFd;
259 std::fs::File::open(path)
260 .map(|file| Fd::owning(file.into_raw_fd()))
261 .map_err(|e| Refusal::Errno {
262 word: WORD,
263 errno: e.raw_os_error().unwrap_or(libc::EIO),
264 })
265 }
266 #[cfg(not(target_os = "linux"))]
267 {
268 let _ = path;
269 Err(Refusal::Absent(WORD))
270 }
271}
272
273pub fn ioctl(fd: &Fd, request: u64, argument: u64) -> Result<i64, Refusal> {
274 const WORD: &str = "fd_ioctl";
275 #[cfg(target_os = "linux")]
276 {
277 seam::ioctl(fd.live(WORD)?, request, argument)
278 .map_err(|errno| Refusal::Errno { word: WORD, errno })
279 }
280 #[cfg(not(target_os = "linux"))]
281 {
282 let _ = (fd, request, argument);
283 Err(Refusal::Absent(WORD))
284 }
285}
286
287#[derive(Debug)]
288pub struct Ring {
289 #[cfg(target_os = "linux")]
290 inner: seam::Ring,
291 #[allow(dead_code)]
292 map: Arc<Fd>,
293}
294
295impl Ring {
296 pub fn open(map: Arc<Fd>, size: usize) -> Result<Self, Refusal> {
297 const WORD: &str = "ringbuf_open";
298 if size == 0 || !size.is_power_of_two() {
299 return Err(Refusal::Bad(format!(
300 "{WORD}: a ring buffer's size is a power of two, got {size}"
301 )));
302 }
303 #[cfg(target_os = "linux")]
304 {
305 let raw = map.live(WORD)?;
306 let inner = seam::Ring::open(raw, size)
307 .map_err(|errno| Refusal::Errno { word: WORD, errno })?;
308 Ok(Self { inner, map })
309 }
310 #[cfg(not(target_os = "linux"))]
311 {
312 let _ = map;
313 Err(Refusal::Absent(WORD))
314 }
315 }
316
317 pub fn next(&self, timeout_ms: i32) -> Result<Option<Vec<u8>>, Refusal> {
318 const WORD: &str = "ringbuf_next";
319 #[cfg(target_os = "linux")]
320 {
321 self.inner
322 .next(timeout_ms)
323 .map_err(|errno| Refusal::Errno { word: WORD, errno })
324 }
325 #[cfg(not(target_os = "linux"))]
326 {
327 let _ = timeout_ms;
328 Err(Refusal::Absent(WORD))
329 }
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn integers_land_at_their_offsets_and_the_rest_stays_zero() {
339 let m = marshal(
340 "t",
341 12,
342 &[
343 Slot::Int {
344 at: 0,
345 width: 4,
346 value: 0x0102_0304,
347 },
348 Slot::Int {
349 at: 8,
350 width: 2,
351 value: 0xABCD,
352 },
353 ],
354 )
355 .unwrap();
356 let mut want = vec![0u8; 12];
357 want[..4].copy_from_slice(&0x0102_0304u32.to_ne_bytes());
358 want[8..10].copy_from_slice(&0xABCDu16.to_ne_bytes());
359 assert_eq!(m.attr, want);
360 }
361
362 #[test]
363 fn an_empty_size_with_no_slots_is_an_empty_struct_and_a_field_past_the_end_is_refused() {
364 assert!(marshal("t", 0, &[]).unwrap().attr.is_empty());
365 let e = marshal(
366 "t",
367 4,
368 &[Slot::Int {
369 at: 2,
370 width: 4,
371 value: 1,
372 }],
373 )
374 .unwrap_err();
375 assert!(e.to_string().contains("does not fit"), "{e}");
376 }
377
378 #[test]
379 fn a_buffer_field_holds_the_address_of_a_buffer_kept_for_the_call() {
380 let m = marshal(
381 "t",
382 16,
383 &[
384 Slot::In {
385 at: 0,
386 bytes: vec![7, 8, 9],
387 },
388 Slot::Out { at: 8, capacity: 5 },
389 ],
390 )
391 .unwrap();
392 let held = m.in_buffers[0].as_ptr() as usize as u64;
393 let out = m.out_buffers[0].as_ptr() as usize as u64;
394 assert_eq!(u64::from_ne_bytes(m.attr[0..8].try_into().unwrap()), held);
395 assert_eq!(u64::from_ne_bytes(m.attr[8..16].try_into().unwrap()), out);
396 assert_eq!(m.in_buffers[0], [7, 8, 9]);
397 assert_eq!(m.out_buffers()[0], [0, 0, 0, 0, 0]);
398 }
399
400 #[test]
401 fn a_strings_field_is_an_array_of_addresses_of_nul_terminated_names() {
402 let m = marshal(
403 "t",
404 8,
405 &[Slot::Strings {
406 at: 0,
407 strings: vec!["ab".into(), "c".into()],
408 }],
409 )
410 .unwrap();
411 assert_eq!(m.in_buffers()[0], [b'a', b'b', 0]);
412 assert_eq!(m.in_buffers()[1], [b'c', 0]);
413 let table = &m.in_buffers()[2];
414 assert_eq!(table.len(), 16);
415 let first = u64::from_ne_bytes(table[..8].try_into().unwrap());
416 assert_eq!(first, m.in_buffers()[0].as_ptr() as usize as u64);
417 let addr = u64::from_ne_bytes(m.attr[..8].try_into().unwrap());
418 assert_eq!(addr, table.as_ptr() as usize as u64);
419 }
420
421 #[test]
422 fn an_empty_buffer_is_a_null_address() {
423 let m = marshal(
424 "t",
425 8,
426 &[Slot::In {
427 at: 0,
428 bytes: vec![],
429 }],
430 )
431 .unwrap();
432 assert_eq!(m.attr, [0u8; 8]);
433 }
434
435 #[test]
436 fn a_descriptor_slot_writes_the_number_and_a_closed_one_is_refused() {
437 let fd = Arc::new(Fd {
438 raw: Mutex::new(Some(41)),
439 });
440 let m = marshal(
441 "t",
442 4,
443 &[Slot::Descriptor {
444 at: 0,
445 fd: fd.clone(),
446 }],
447 )
448 .unwrap();
449 assert_eq!(m.attr, 41i32.to_ne_bytes());
450 *fd.raw.lock().unwrap() = None;
451 let e = marshal("t", 4, &[Slot::Descriptor { at: 0, fd }]).unwrap_err();
452 assert!(e.to_string().contains("closed"), "{e}");
453 }
454
455 #[test]
456 fn a_width_that_is_no_integer_width_is_refused() {
457 let e = marshal(
458 "t",
459 8,
460 &[Slot::Int {
461 at: 0,
462 width: 3,
463 value: 1,
464 }],
465 )
466 .unwrap_err();
467 assert!(e.to_string().contains("1, 2, 4 or 8"), "{e}");
468 }
469
470 #[test]
471 fn a_ring_size_that_is_not_a_power_of_two_is_refused_before_any_call() {
472 let map = Arc::new(Fd {
473 raw: Mutex::new(None),
474 });
475 let e = Ring::open(map, 3).unwrap_err();
476 assert!(e.to_string().contains("power of two"), "{e}");
477 }
478
479 #[cfg(target_os = "linux")]
480 #[test]
481 fn on_linux_a_directory_opens_and_a_missing_path_answers_its_errno() {
482 let dir = open_read("/").expect("the root directory opens");
483 assert!(dir.number().is_some());
484 assert_eq!(
485 open_read("/no/such/path").map(|_| ()),
486 Err(Refusal::Errno {
487 word: "fd_open",
488 errno: libc::ENOENT
489 })
490 );
491 }
492
493 #[cfg(not(target_os = "linux"))]
494 #[test]
495 fn off_linux_every_call_is_a_named_absence() {
496 assert_eq!(
497 bpf(0, 0, &[], false).unwrap_err(),
498 Refusal::Absent("bpf_syscall")
499 );
500 assert_eq!(
501 perf_event_open(8, &[], 0, 0, None, 0).unwrap_err(),
502 Refusal::Absent("perf_event_open")
503 );
504 assert_eq!(open_read("/").map(|_| ()), Err(Refusal::Absent("fd_open")));
505 }
506
507 #[cfg(target_os = "linux")]
508 #[test]
509 fn on_linux_a_bad_command_is_an_errno_never_a_crash() {
510 let a = bpf(9_999, 8, &[], false).unwrap();
511 assert!(a.errno.is_some());
512 assert_eq!(a.ret, -1);
513 }
514}