Skip to main content

blue_lang_kernel/
lib.rs

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}