Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions issues/fstats-answer-is-declared-twice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
status: open
kind: defect
opened: 2026-10-07
---

# `fstat`'s answer is declared twice, and nothing holds the two layouts together

`SYS_FSTAT` copies out `kernel/src/object/ops.rs`'s `Stat`, three `u64`s, and
`toyos_abi::syscall::fstat` reads the answer into `toyos-abi/src/syscall.rs`'s
`Stat`, whose first field is `FileType`, a `#[repr(u64)]` enum. The kernel's
copy exists because a struct holding the enum is not valid for every bit
pattern and so cannot be `UserSafe`; it is sound, since the kernel only
writes it, with `FileType::… as u64`.

Read, nothing run: the two declarations agree today field for field. A field
added to one and not the other compiles on both sides, and userland then reads
a layout the kernel did not write.

## Exit condition

One declaration of the answer's layout that both the kernel's copy-out and
`toyos_abi::syscall::fstat` read, or a compile-time assertion beside one of
them that fails when the two differ in size or in any field's offset.

## Owner

`toyos-abi/src/syscall.rs`, `kernel/src/object/ops.rs`. Nobody holds it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
status: open
kind: defect
opened: 2026-10-08
---

# Three kernel byte views still rest on a layout claim made by hand

`toyos_abi::usersafe::bytes` is the one view of a `UserSafe` value as bytes,
and `user_safe!` is what proves no byte of the value is padding. Three structs
the kernel copies out are not declared through the macro, and each reaches
user memory through an `unsafe` `from_raw_parts` of the kernel's own whose
`SAFETY` comment cites an assertion beside the declaration:

| struct (`toyos-abi/src/pci.rs`) | the kernel's view | what the comment rests on |
|---|---|---|
| `DmaGrant` | `grant_bytes`, `kernel/src/syscall/device.rs` | `size_of == 4 + 4 + 8 + 8`, a sum written by hand |
| `DmaMapping` | inline in `sys_device_dma_map`, the same file | `size_of == 8 + 8`, a sum written by hand |
| `DeviceIrqRecord` | `record_bytes`, `kernel/src/object/ops.rs` | `SIZE == 4`, a total written by hand; the struct is one `u32` |

Read, nothing run: none of the three has padding today. Each assertion
compares the struct's size with a number a person wrote, and nothing ties
that number to the fields: a field added to any of the three fails the build
until the number is moved, and passes once it is moved to the new size,
whether or not that size holds a gap. The bytes of a gap are the kernel's
stack.

Found while the eleven `as_bytes` in `toyos-abi` moved to `usersafe::bytes`;
these three were outside that change's brief.

## Exit condition

The three are declared through `user_safe!`, so a byte of any that belongs to
no field fails the build with the macro's message; the kernel's three
`from_raw_parts` over them are `usersafe::bytes`, and the two hand sums are
deleted.

## Owner

`toyos-abi/src/pci.rs`, `kernel/src/syscall/device.rs`,
`kernel/src/object/ops.rs`. Nobody holds it.
59 changes: 0 additions & 59 deletions issues/usersafe-layouts-are-checked-by-hand.md

This file was deleted.

21 changes: 10 additions & 11 deletions kernel/src/log/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
//!
//! [`Rights::LOG`]: toyos_abi::handle::Rights::LOG

use toyos_abi::log::{LogCursor, LogRecord};
use toyos_abi::log::LogCursor;
use toyos_abi::syscall::SyscallError;
use toyos_abi::UserSafe;

use crate::watch::Watch;
use crate::user_ptr::UserBytesMut;
Expand All @@ -24,19 +25,18 @@ pub fn post_readiness() {
}

// Fixed stride, never packed: the caller indexes by shift, so the kernel does no length arithmetic.
struct UserRecords<'a, 'b, R> {
struct UserRecords<'a, 'b> {
out: &'a mut UserBytesMut<'b>,
written: usize,
capacity: usize,
bytes: fn(&R) -> &[u8],
}

impl<R> RecordSink<R> for UserRecords<'_, '_, R> {
impl<R: UserSafe> RecordSink<R> for UserRecords<'_, '_> {
fn put(&mut self, record: &R) -> bool {
if self.written >= self.capacity {
return false;
}
let bytes = (self.bytes)(record);
let bytes = toyos_abi::usersafe::bytes(record);
self.out.write_at(self.written * bytes.len(), bytes);
self.written += 1;
true
Expand All @@ -49,20 +49,19 @@ pub fn read(
out: &mut UserBytesMut,
capacity: usize,
) -> Result<usize, SyscallError> {
read_rings(&super::shards(), cursor, out, capacity, LogRecord::as_bytes)
read_rings(&super::shards(), cursor, out, capacity)
}

/// Copies records `cursor` has not seen in `rings` into `out`, oldest first,
/// each as `bytes` writes it; never blocks.
/// Copies records `cursor` has not seen in `rings` into `out`, oldest first;
/// never blocks.
pub fn read_rings<const W: usize, const N: usize>(
rings: &Rings<W, N>,
cursor: &mut LogCursor,
out: &mut UserBytesMut,
capacity: usize,
bytes: fn(&<Ring<W, N> as Stream>::Record) -> &[u8],
) -> Result<usize, SyscallError>
where
Ring<W, N>: Stream,
Ring<W, N>: Stream<Record: UserSafe>,
{
let shards = rings.iter().flatten().count() as u32;
// Refused, not truncated: a capacity below one record per ring cannot hold what a single call may have to merge.
Expand All @@ -73,7 +72,7 @@ where
let Some(mut walk) = Cursor::from_reader(cursor, rings) else {
return Err(SyscallError::InvalidArgument);
};
let mut sink = UserRecords { out, written: 0, capacity, bytes };
let mut sink = UserRecords { out, written: 0, capacity };
drain_ordered(rings, &mut walk, &mut sink);
let written = sink.written;

Expand Down
13 changes: 7 additions & 6 deletions kernel/src/object/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use core::sync::atomic::{AtomicBool, Ordering};

use toyos_abi::handle::{RawHandle, Rights};
use toyos_abi::syscall::SyscallError;
use toyos_abi::usersafe::bytes;
use toyos_abi::FramebufferInfo;

use crate::device::{Claim, DeviceType};
Expand Down Expand Up @@ -70,23 +71,23 @@ impl DeviceInfo {
)?;
info.scanout = [h[0], h[1]];
info.cursor = h[2];
info.as_bytes().into()
bytes(&info).into()
}
// Nothing to install: every address in it is a size, and the memory
// is what `SYS_DEVICE_DMA_ALLOC` answers later.
Self::PciFunction(info, _) => info.as_bytes().into(),
Self::Partition(info) => info.as_bytes().into(),
Self::PciFunction(info, _) => bytes(info).into(),
Self::Partition(info) => bytes(info).into(),
Self::Isa(set, _) => set.wire().iter().flat_map(|word| word.to_ne_bytes()).collect(),
Self::Acpi(info, _) => info.as_bytes().into(),
Self::Acpi(info, _) => bytes(info).into(),
Self::Hda(info, pcm) => {
let mut info = *info;
info.pcm = install_buffers(table, &[pcm])?[0];
info.as_bytes().into()
bytes(&info).into()
}
Self::VirtioSound(info, dma) => {
let mut info = *info;
info.dma = install_buffers(table, &[dma])?[0];
info.as_bytes().into()
bytes(&info).into()
}
})
}
Expand Down
17 changes: 9 additions & 8 deletions kernel/src/object/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ pub fn read_device(
let mut count = 0;
while count + event_size <= buf.len() {
let Some(event) = keyboard::try_read_event() else { break };
buf.write_at(count, event.as_bytes());
buf.write_at(count, toyos_abi::usersafe::bytes(&event));
count += event_size;
}
if count > 0 { Some(count as u64) } else { None }
Expand All @@ -405,7 +405,7 @@ pub fn read_device(
let mut count = 0;
while count + event_size <= buf.len() {
let Some(event) = mouse::try_read_event() else { break };
buf.write_at(count, event.as_bytes());
buf.write_at(count, toyos_abi::usersafe::bytes(&event));
count += event_size;
}
if count > 0 { Some(count as u64) } else { None }
Expand Down Expand Up @@ -608,12 +608,13 @@ pub fn seek(object: &KObjectRef, pos: SeekFrom) -> u64 {
})
}

#[repr(C)]
#[derive(Clone, Copy)]
pub struct Stat {
pub file_type: u64,
pub size: u64,
pub mtime: u64,
toyos_abi::user_safe! {
#[derive(Clone, Copy)]
pub struct Stat {
pub file_type: u64,
pub size: u64,
pub mtime: u64,
}
}

/// What kind of thing this is, and how big.
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/syscall/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub(super) fn sys_trace_read(
/// A read of records on a cursor the caller holds, under `need`.
///
/// A copy-out failure after a successful read costs the caller those records; the cursor round-trips through the caller's own memory.
fn read_on_cursor<C: crate::user_ptr::UserSafe>(
fn read_on_cursor<C: toyos_abi::UserSafe>(
ctx: &SyscallContext,
syscap: RawHandle,
need: Rights,
Expand Down
4 changes: 2 additions & 2 deletions kernel/src/syscall/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ pub(super) fn sys_query_modules(out: &mut UserBytesMut) -> u64 {
path_offset,
path_len: exe_path_bytes.len() as u32,
};
out.write_at(0, exe_info.as_bytes());
out.write_at(0, toyos_abi::usersafe::bytes(&exe_info));
out.write_at(path_offset as usize, exe_path_bytes);
path_offset += exe_path_bytes.len() as u32;

Expand All @@ -534,7 +534,7 @@ pub(super) fn sys_query_modules(out: &mut UserBytesMut) -> u64 {
path_offset,
path_len: lib_path_bytes.len() as u32,
};
out.write_at((1 + i) * info_size, lib_info.as_bytes());
out.write_at((1 + i) * info_size, toyos_abi::usersafe::bytes(&lib_info));
out.write_at(path_offset as usize, lib_path_bytes);
path_offset += lib_path_bytes.len() as u32;
}
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ fn rings() -> Rings<WORDS, SLOTS> {

/// Copies records `cursor` has not seen into `out`, oldest first; never blocks.
pub fn read(cursor: &mut TraceCursor, out: &mut UserBytesMut, capacity: usize) -> Result<usize, SyscallError> {
crate::log::user::read_rings(&rings(), &mut cursor.0, out, capacity, TraceRecord::as_bytes)
crate::log::user::read_rings(&rings(), &mut cursor.0, out, capacity)
}

/// [`Kind::Mark`] records `count` of them on this CPU, `data` counting up, and
Expand Down
43 changes: 1 addition & 42 deletions kernel/src/user_ptr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,55 +18,14 @@ use alloc::vec::Vec;
use core::marker::PhantomData;

use toyos_abi::syscall::SyscallError;
use toyos_abi::UserSafe;
use toyos_userbound::{Pinned, Pins, Segment};

use crate::UserAddr;

/// Longest string, in bytes, the kernel accepts from userspace; one bound for every string syscall, since each copies or tokenizes rather than streaming.
pub const MAX_USER_STR: u64 = 64 * 1024;

/// Marker for types safe to interpret from / write to validated user pointers.
/// # Safety
/// Must be `#[repr(C)]`, `Copy`, have no padding, and be valid for any bit pattern.
pub unsafe trait UserSafe: Copy {}

// Every impl below is hand-checked: `#[repr(C)]`, `Copy`, integer fields only, explicit `_pad` for every alignment gap — Rust cannot verify this mechanically. A padding byte would leak kernel stack out through `copy_out` or accept an unwritten value through `copy_in`.

// SAFETY: a primitive integer (and an array of them) is `#[repr(C)]`, has no padding, and every bit pattern is a value.
unsafe impl UserSafe for u32 {}
// SAFETY: see `u32`.
unsafe impl UserSafe for u64 {}
// SAFETY: see `u32` — an array adds no padding between elements.
unsafe impl UserSafe for [u32; 2] {}
// SAFETY: see `u32`.
unsafe impl UserSafe for [u64; 2] {}

// SAFETY: `#[repr(C)] Copy`, three `u64`s, no padding; `file_type` is a `u64`, not the enum it names, so every bit pattern stays valid.
unsafe impl UserSafe for crate::object::ops::Stat {}

// SAFETY: `#[repr(C)] Copy`, fourteen `u64`s, no padding; every field is validated where it is used, not here.
unsafe impl UserSafe for toyos_abi::syscall::SpawnArgs {}
// SAFETY: `#[repr(C)] Copy`, `RawHandle`, a `flags: u32`, then six `u64`s — no padding.
unsafe impl UserSafe for toyos_abi::syscall::NamespaceBuild {}
// SAFETY: `#[repr(C)] Copy`, `u64`, `u64`, `i64` — 24 bytes, no padding.
unsafe impl UserSafe for toyos_abi::syscall::SchedInfo {}
// SAFETY: `#[repr(C)] Copy`; the two `u32` pairs keep every `u64` 8-aligned, so there is no padding.
unsafe impl UserSafe for toyos_abi::syscall::ProcessStats {}
// SAFETY: `#[repr(C)] Copy`, `[RawHandle; 2]` then six `u32`s — align 4, no padding.
unsafe impl UserSafe for toyos_abi::FramebufferInfo {}
// SAFETY: `#[repr(C)] Copy`, `RawHandle`, an explicit `_pad: u32`, `u64` — no padding.
unsafe impl UserSafe for toyos_abi::syscall::InboxSetup {}

// SAFETY: `#[repr(C)] Copy`, two `u8`s, no padding; `keycode`/`modifiers` are plain `u8`, not enums, so any bit pattern is valid.
unsafe impl UserSafe for toyos_abi::input::RawKeyEvent {}
// SAFETY: `#[repr(C)] Copy`, `u8`, `i8`, `u16`, `u16` — 2-aligned, no padding.
unsafe impl UserSafe for toyos_abi::input::MouseEvent {}

// SAFETY: `#[repr(C)] Copy`, 88 bytes with no padding (checked by a compile-time size assertion); every field is clamped where it is used, not here.
unsafe impl UserSafe for toyos_abi::log::LogCursor {}
// SAFETY: `#[repr(transparent)]` over the `LogCursor` above.
unsafe impl UserSafe for toyos_abi::trace::TraceCursor {}

pub(crate) use toyos_userbound::Access;

fn translate_now(
Expand Down
Loading
Loading