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 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}