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
6 changes: 3 additions & 3 deletions build/libcxx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ pub(crate) const OPTIONS: [(&str, &str); 18] = [
("LIBCXXABI_HAS_CXA_THREAD_ATEXIT_IMPL", "ON"),
// The C library has no `dladdr`, so libunwind names no function it unwinds.
("LIBUNWIND_ADDITIONAL_COMPILE_FLAGS", "-D_LIBUNWIND_USE_DLADDR=0"),
// `open` refuses every directory, so no descriptor names one for `openat`
// and `unlinkat` to resolve against: `remove_all` walks a directory
// iterator, as libc++'s Windows does
// The C library has no `O_NOFOLLOW`, which libc++'s descriptor walk opens
// each directory with so a link swapped in is not followed: `remove_all`
// walks a directory iterator, as libc++'s Windows does
// (`issues/remove-all-follows-a-link-swapped-in-mid-walk.md`).
("LIBCXX_ADDITIONAL_COMPILE_FLAGS", "-DREMOVE_ALL_USE_DIRECTORY_ITERATOR"),
("LIBCXX_INCLUDE_BENCHMARKS", "OFF"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
status: open
kind: defect
opened: 2026-10-23
---

# A served file keeps one name and no access time until the node core

File protocol v2 (`sdk/toyos/src/fs.rs`) carries hard links, `LINK_REPLACE`,
unnamed files, access and change times and a link count. Today's servers
are path-keyed (`system/fileserver/src/volume.rs`) and keep none of them, so
`system/fileserver/src/main.rs` answers:

- `LINK` and `CREATE_UNNAMED`: `Unsupported`, so libc's `link`, `linkat` and
`O_TMPFILE` and std's `hard_link` fail `ENOTSUP`;
- an access time to set: `Unsupported`, so `utimensat` and `utimes` with a
null `times`, which set both times, fail `ENOTSUP`; a modification time is
set on DATA's files and refused on FAT's and on a directory;
- a link count of 1, no access or change time, and the server's pid as the
volume, so `st_dev` changes at a restart;
- a held directory by its volume path, which the server moves with each
rename it makes and marks removed at its removal.

The kernel's `/tmp` keeps no second name, unnamed file or set time
(`sdk/toyos/src/files.rs`, `NotSupported`), and a descriptor of one of its
directories names its absolute path, so a path from it is that path's,
lexically, and a rename of the directory is not followed.

`tests/corpus/tinycc/214_at_calls.c` asserts each refusal on `/home` and
`/tmp`.

**Exit**: the node core and its memory-format adapter (storage C2) serve
`LINK`, `LINK_REPLACE`, `CREATE_UNNAMED`, the access time and the format's
link count, and the bcachefs adapter (C3) does on a durable DATA with
`st_dev` derived from the volume's UUID; `214_at_calls`' refusals on `/home`
become their effects. `/tmp` served by the memory format (storage W3) ends
the kernel's half.
33 changes: 33 additions & 0 deletions issues/a-working-directory-path-reads-its-dot-dot-off-the-text.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
status: open
kind: defect
opened: 2026-10-23
---

# A working-directory path reads its `..` off the text

`toyos::files` makes every absolute path, and every path from the working
directory, absolute and drops its `..` by text before it picks the server
(`sdk/toyos/src/files.rs`, `absolute`; `files/owner.rs`, `place`). So
`/home/link/../x`, where `link` is a symlink to `a/b` on `/home`, opens
`/home/x`, where POSIX resolves `..` from the directory `link` led to and
opens `/home/a/x`.

A path from a directory descriptor does not do this. Its `..` goes to the file
server, which resolves it from the directory itself
(`system/fileserver/src/resolve.rs`). So within one process, `openat(fd, "link/../x")`
and `open("/home/link/../x")` open two different files, and libc's `realpath`,
which resolves a served name POSIX's way (`sdk/libc/src/resolve.rs`), names the
first.

The text rule has one real job: a `..` that climbs out of a capability's
directory (`/home/x/../../system`) leads to another server, so the client
must settle that climb in its own table before it routes the path.

**Owner**: the SDK's file client, `sdk/toyos/src/files.rs`.

**Exit**: `toyos::files` hands each component after a path's capability
prefix, `..` included, to that capability's server. It resolves a `..` that
climbs above the capability's directory against the process's own table. A
guest case opens `/home/link/../x` with `link` → `a/b`, both through
`AT_FDCWD` and from a descriptor, and gets `/home/a/x` both times.

This file was deleted.

22 changes: 0 additions & 22 deletions issues/libc-has-no-pread-or-pwrite.md

This file was deleted.

4 changes: 1 addition & 3 deletions issues/libc-refuses-what-toyos-cannot-yet-answer.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@ These answer failure in their POSIX form and do nothing
(`sdk/libc/src/refused.rs`, `posix_io.rs`), each asserted by
`tests/corpus/tinycc/206_libc_refusals.c`. Each waits on what it names:

- `link`, `ENOSYS`: no call gives a file a second name.
- `symlink` of a link the kernel would hold, `ENOSYS`: `SYS_SYMLINK`
displaces what its name held
(`issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md`). A
file server makes one and refuses a name that exists, as POSIX has it.
- `chmod` and `fchmod`, `ENOSYS`: a file has no mode bits to set.
- `utimes`, `ENOSYS`: no call sets a file's times.
- `chmod`, `fchmod` and `fchmodat`, `ENOSYS`: a file has no mode bits to set.
- `statvfs` and `fstatvfs`, `ENOSYS`: no call answers a filesystem's size or
free space.
- `getrlimit` and `setrlimit`, `ENOSYS`: no call answers a process's limits.
Expand Down
9 changes: 5 additions & 4 deletions issues/remove-all-follows-a-link-swapped-in-mid-walk.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ outside the tree it was asked to remove. libc++ says so of that walk
https://reviews.llvm.org/D118134), the class of CVE-2022-21658. Its other walk
holds a descriptor for each directory and resolves every name against it
(`openat`, `fdopendir`, `unlinkat`, `O_DIRECTORY`, `O_NOFOLLOW`,
`AT_REMOVEDIR`), and ToyOS has no such descriptor: `open` refuses every
directory, and the kernel resolves every path from the root.
`AT_REMOVEDIR`). libc has each of those but `O_NOFOLLOW`, and a descriptor of
a directory the kernel's `/tmp` holds names its path, which the kernel
resolves from the root (`sdk/toyos/src/files.rs`).

Owner: `issues/toyos-builds-itself.md`, beside its path, since no exit there
needs it; its M2 clang brought `std::filesystem` in.

**Exit**: a directory opens as a handle that the kernel resolves names
against, libc defines `openat`, `fdopendir` and `unlinkat` over it, and
**Exit**: libc's `openat` takes `O_NOFOLLOW`, a descriptor of every
directory names the directory itself and not its path, and
`build/libcxx.rs` passes no `REMOVE_ALL_USE_DIRECTORY_ITERATOR`.
14 changes: 0 additions & 14 deletions issues/std-set-times-answers-ok-and-sets-nothing.md

This file was deleted.

6 changes: 4 additions & 2 deletions issues/the-kernel-resolves-a-path-by-rules-of-its-own.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ A path on the kernel's own mounts, ROOT and `/tmp`, is resolved by
which libc's `open` answers `EINVAL`; POSIX's answer is `ELOOP`, which libc's
`realpath` gives.

A file server resolves its own paths POSIX's way, with a bound of its own
A file server resolves what it is handed POSIX's way, with a bound of its own
(`system/fileserver/src/resolve.rs`, `MAX_LINKS`), so the same link reads
differently on `/tmp` and on a served directory. libc's `realpath`
differently on `/tmp` and on a served directory. It is handed no `..` of an
absolute or working-directory path, which the client reads off the text first
(`issues/a-working-directory-path-reads-its-dot-dot-off-the-text.md`). libc's `realpath`
(`sdk/libc/src/resolve.rs`, `resolve_path`) resolves the names a file server
holds POSIX's way and hands the kernel the rest at the first name it holds,
answering that part by the kernel's rules, so that a program that
Expand Down
2 changes: 1 addition & 1 deletion issues/toyos-builds-itself.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ An owner is an issue under `issues/`, or this track.
| # | Item | Status | Owner | Blocked on |
|---|---|---|---|---|
| 7 | Threads mature enough for LLVM | not started | `std-and-libc-drop-the-answer-thread-join-gives` | — |
| 8 | Concurrent reads and writes of one file: `pread` and `pwrite`. Without them LLVM reads a file slice with `lseek` and `read`, which moves an offset other threads share | not started | `libc-has-no-pread-or-pwrite` | — |
| 8 | Concurrent reads and writes of one file: `pread` and `pwrite`, on a served file and a kernel one, which never move a shared offset (`tests/corpus/tinycc/214_at_calls.c`) | done | this track | — |
| 9 | LLVM, clang and lld built by bootstrap for a ToyOS host, and the libc names they call | not started | `bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host`, `rustc-llvm-cannot-build-for-a-toyos-host`, `libc-lacks-names-llvm-for-a-toyos-host-calls`, `a-toyos-hosted-llvm-is-configured-as-running-on-the-build-machine` | — |
| 10 | A std binary links the C++ runtime | not started | `a-rust-std-binary-cannot-link-the-cxx-runtime` | 9 |
| 11 | A package ships its own libraries (`librustc_driver`); moot for M2's static PIEs | not started | `a-package-cannot-ship-its-own-libraries` | 3 |
Expand Down
138 changes: 74 additions & 64 deletions kernel/src/object/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,42 +391,82 @@ fn close_ends_polls(object: &KObjectRef) -> bool {

fn read_file(file: &FileObject, buf: &mut UserBytesMut) -> Option<u64> {
file.with(|state| {
let size = file_cache::size(state.file_id) as usize;
let available = size.saturating_sub(state.position);
let count = buf.len().min(available);
if count == 0 {
return Some(0);
}
let mut read = 0;
let mut refused = false;
while read < count {
let abs_pos = state.position + read;
let page_idx = (abs_pos / 4096) as u32;
let offset_in_page = abs_pos % 4096;
let remaining_in_page = 4096 - offset_in_page;
let to_read = remaining_in_page.min(count - read);
// A refused page is not a page of zeros: stop short rather than fake a hole under a success.
if file_cache::read_page(
state.file_id,
page_idx,
offset_in_page,
&mut buf.sub(read, to_read),
)
.is_err()
{
refused = true;
break;
}
read += to_read;
}
if read == 0 && refused {
return Some(SyscallError::Io.to_u64());
}
let (answer, read) = read_file_at(state.file_id, state.position, buf);
state.position += read;
Some(read as u64)
Some(answer)
})
}

/// Read file `file_id` at `position` into `buf`: the syscall's answer, and how
/// many bytes it read.
fn read_file_at(file_id: file_cache::FileId, position: usize, buf: &mut UserBytesMut) -> (u64, usize) {
let size = file_cache::size(file_id) as usize;
let count = buf.len().min(size.saturating_sub(position));
let mut read = 0;
let mut refused = false;
while read < count {
let abs_pos = position + read;
let page_idx = (abs_pos / 4096) as u32;
let offset_in_page = abs_pos % 4096;
let to_read = (4096 - offset_in_page).min(count - read);
// A refused page is not a page of zeros: stop short rather than fake a hole under a success.
if file_cache::read_page(file_id, page_idx, offset_in_page, &mut buf.sub(read, to_read)).is_err() {
refused = true;
break;
}
read += to_read;
}
if read == 0 && refused {
return (SyscallError::Io.to_u64(), 0);
}
(read as u64, read)
}

/// Write `buf` to file `file_id` at `position`, stamping it: the syscall's
/// answer, and how many bytes it wrote.
fn write_file_at(state: &mut OpenFileState, position: usize, buf: &UserBytes) -> (u64, usize) {
// Refused, never wrapped: past this the `u32` page index would alias a low page.
if (position as u64).saturating_add(buf.len() as u64) > file_cache::MAX_FILE_SIZE {
return (SyscallError::InvalidArgument.to_u64(), 0);
}
let mut written = 0;
let mut refused = false;
while written < buf.len() {
let abs_pos = position + written;
let page_idx = (abs_pos / 4096) as u32;
let offset_in_page = abs_pos % 4096;
let to_write = (4096 - offset_in_page).min(buf.len() - written);
// A page that cannot be re-read off the device is refused, not merged into zeros.
if file_cache::write_page(state.file_id, page_idx, offset_in_page, &buf.sub(written, to_write)).is_err() {
refused = true;
break;
}
written += to_write;
}
if written == 0 && refused {
return (SyscallError::Io.to_u64(), 0);
}
state.mtime = crate::clock::mtime_now();
file_cache::touch(state.file_id, state.mtime);
(written as u64, written)
}

/// Read a file at `offset`, leaving its position: any other object is
/// refused `NotSupported`, as a pipe has no offset to read at.
pub fn pread(object: &KObjectRef, offset: u64, buf: &mut UserBytesMut) -> u64 {
let KObjectRef::File(file) = object else { return SyscallError::NotSupported.to_u64() };
let Ok(position) = usize::try_from(offset) else { return 0 };
file.with(|state| read_file_at(state.file_id, position, buf).0)
}

/// Write a file at `offset`, leaving its position: any other object is
/// refused `NotSupported`.
pub fn pwrite(object: &KObjectRef, offset: u64, buf: &UserBytes) -> u64 {
let KObjectRef::File(file) = object else { return SyscallError::NotSupported.to_u64() };
let Ok(position) = usize::try_from(offset) else { return SyscallError::InvalidArgument.to_u64() };
file.with(|state| write_file_at(state, position, buf).0)
}

/// Read a device claim; takes `table` because describing a device installs handles into it.
pub fn read_device(
claim: &DeviceClaim,
Expand Down Expand Up @@ -562,39 +602,9 @@ fn write_pipe(object: &KObjectRef, buf: &UserBytes) -> Option<u64> {
pub fn try_write(object: &KObjectRef, buf: &UserBytes) -> Option<u64> {
match object {
KObjectRef::File(f) => f.with(|state| {
// Refused, never wrapped: past this the `u32` page index would alias a low page.
if (state.position as u64).saturating_add(buf.len() as u64) > file_cache::MAX_FILE_SIZE {
return Some(SyscallError::InvalidArgument.to_u64());
}
let mut written = 0;
let mut refused = false;
while written < buf.len() {
let abs_pos = state.position + written;
let page_idx = (abs_pos / 4096) as u32;
let offset_in_page = abs_pos % 4096;
let remaining_in_page = 4096 - offset_in_page;
let to_write = remaining_in_page.min(buf.len() - written);
// A page that cannot be re-read off the device is refused, not merged into zeros.
if file_cache::write_page(
state.file_id,
page_idx,
offset_in_page,
&buf.sub(written, to_write),
)
.is_err()
{
refused = true;
break;
}
written += to_write;
}
if written == 0 && refused {
return Some(SyscallError::Io.to_u64());
}
let (answer, written) = write_file_at(state, state.position, buf);
state.position += written;
state.mtime = crate::clock::mtime_now();
file_cache::touch(state.file_id, state.mtime);
Some(written as u64)
Some(answer)
}),
KObjectRef::PipeWrite(_) | KObjectRef::Connection(_) => write_pipe(object, buf),
// The whole lines `klogd`'s queue has room for, and a trailing partial one:
Expand Down
8 changes: 8 additions & 0 deletions kernel/src/syscall/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,14 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) ->
},
SYS_MMAP => sys_mmap(a1, a2, MmapProt(a3), MmapFlags(a4)),
SYS_MUNMAP => sys_munmap(a1, a2),
SYS_PREAD => {
let Some(mut buf) = ctx.user_bytes_mut(UserAddr::new(a2), a3) else { return bad_addr };
with_object(RawHandle(a1 as u32), Rights::READ, |o| ops::pread(o, a4, &mut buf))
}
SYS_PWRITE => {
let Some(buf) = ctx.user_bytes(UserAddr::new(a2), a3) else { return bad_addr };
with_object(RawHandle(a1 as u32), Rights::WRITE, |o| ops::pwrite(o, a4, &buf))
}
SYS_READ_NONBLOCK => {
let Some(mut buf) = ctx.user_bytes_mut(UserAddr::new(a2), a3) else { return bad_addr };
sys_read_nonblock(RawHandle(a1 as u32), &mut buf)
Expand Down
Loading