Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
3aa7c07
stream region: a connect commits 132 KiB, and each ring doubles under…
Japabu Oct 28, 2026
e1e016b
stream region: the growth models, controls, tests and the commit gues…
Japabu Oct 28, 2026
a140dcd
stream region: the issue record keeps what is left, and the node's pa…
Japabu Oct 28, 2026
9f250dc
stream region: the guest's own netstack servers make their receive ri…
Japabu Oct 28, 2026
3d84dad
stream region: the growth model borrows its words as the others do
Japabu Oct 28, 2026
928fd0c
review r1: no uncommitted page reaches the kernel's reads, a syscall'…
Japabu Oct 28, 2026
e2510f3
stream region: onto main, the paged objects' frees and the clock page
Japabu Nov 1, 2026
ababbf4
issues: a connect's region commits 132 KiB, and its T14 reading is owed
Japabu Nov 1, 2026
13a9ad6
stream region: a loopback stream's bytes through its rings' growth, o…
Japabu Nov 1, 2026
f0e75f2
stream region: the growth test's peer is the harness, as ToyOS has no…
Japabu Nov 1, 2026
54c0009
stream region: netstack's receive ring growth, through [tcp], on the …
Japabu Nov 1, 2026
99f5c3b
stream region: each producer's commit alone holds its growth, on the …
Japabu Nov 1, 2026
f6363b9
review r2: netstack's and the client's consumer commits are pinned, a…
Japabu Nov 1, 2026
a97ce12
space: a syscall's window is judged committed in the one descent that…
Japabu Nov 1, 2026
1ff5a6f
stream region: the download test names its local endpoint, as #1147 m…
Japabu Nov 3, 2026
2798d7f
shm: a syscall's window over an object committed whole asks one word,…
Japabu Nov 3, 2026
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
2 changes: 2 additions & 0 deletions build/ci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,8 @@ pub(crate) const CONTROLS: &[Control] = &[
)]),
red(SDK, "transport-no-resume", Some("transport_loom"), &[Fails("two_holders_read_each_byte_once")]),
red(SDK, "transport-ungated-violation", Some("transport_loom"), &[Fails("looks_without_the_gate_lose_no_wake")]),
red(SDK, "transport-grow-room-now", Some("transport_loom"), &[Fails("a_ring_grown_while_it_holds_bytes_reads_each_as_written")]),
red(SDK, "transport-size-before-tail", Some("transport_loom"), &[Fails("a_ring_grown_while_it_holds_bytes_reads_each_as_written")]),
red(SDK, "transport-end-keeps-inflight", None, &[Fails(
"transport::inflight::tests::an_end_answers_every_tag_once_and_a_late_completion_nothing",
)]),
Expand Down
19 changes: 19 additions & 0 deletions issues/a-process-holds-at-most-128-streams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
status: open
kind: defect
opened: 2026-10-14
---

# A process holds at most 128 streams

The process's ends are a fixed table, `MAX_STREAMS` = 128 streams and 256
handles of their connections (`sdk/toyos/src/net/stream.rs`), since `toyos`
allocates nothing: past it a connect or an accept is refused
`ResourceExhausted` before anything is made. 128 is libc's `MAX_SOCKETS`, an
estimate no measurement set, and a new limit for a std program.

**Exit condition**: the table grown on demand, or its bound read off a
measured program's need.

**Owner**: the orchestrator, who stages
`issues/netstack-moves-every-byte-through-two-kernel-pipes.md`.
27 changes: 0 additions & 27 deletions issues/a-stream-commits-eight-mebibytes-and-a-process-holds-128.md

This file was deleted.

23 changes: 23 additions & 0 deletions issues/a-stream-ring-never-shrinks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
status: open
kind: defect
opened: 2026-10-28
---

# A stream's ring never shrinks

A stream region commits its header and 64 KiB of each ring at the connect,
and each ring's producer doubles it, up to 4 MiB, when it needs more
(`toyos::transport::stream`). Nothing gives a page back: a long-lived stream
that once carried a burst keeps the most it ever held, up to 8 MiB + 4 KiB,
for as long as it lives, counted to its client's job. netstack prices each
place at that most (`PLACE_BYTES`, `system/netstack/src/main.rs`).

**Owner**: the orchestrator, who assigns it after #1077 lands.

**Exit**: a ring whose stream has gone idle gives back what it no longer holds,
at a quiescent point both ends agree on (both cursors equal, under the
session's gates), through a kernel call that decommits pages of a memory
object; a host test of the ring and a loom model of the shrink beside a
growth and a write, and a guest test that reads the client's account fall
after an idle stream's shrink.
9 changes: 5 additions & 4 deletions issues/a-tcp-connect-zeroes-twelve-mebibytes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ opened: 2026-10-27
Before a ToyOS TCP connect returns, it makes and zeroes memory the connection
may never use:

- **The stream region**, 8 MiB (`toyos::net::stream::REGION_BYTES`).
`SYS_SHM_CREATE` takes and zeroes each of its 2048 frames before it answers
(`kernel/src/object/shm.rs`), and netstack maps all of them before the SYN
leaves.
- **The stream region**, 8 MiB (`toyos::net::stream::REGION_BYTES`), whose
2048 frames `SYS_SHM_CREATE` took and zeroed before it answered. Since
#1077 a connect commits its header and 64 KiB of each ring, 132 KiB, and
each ring's producer commits more as it doubles the ring
(`toyos::transport::stream`); the T14's reading of that is owed.
- **The connection's two directions**, 2 MiB each: the request's page on the
client's write and the answer's on netstack's, each zeroed whole on that
first write (`issues/a-pipe-is-a-two-mebibyte-page-for-a-mapping-nothing-makes.md`).
Expand Down
11 changes: 6 additions & 5 deletions kernel/pure/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,18 +214,19 @@ impl<O: Object, S: Object> Inventory<O, S> {

/// Whether entries cover every page of `[start, end)`.
pub fn covered(&self, start: u64, end: u64) -> bool {
self.covered_where(start, end, |_| true)
self.covered_where(start, end, |_, _| true)
}

/// Whether entries cover every page of `[start, end)` and each that meets
/// it answers `ok`: one descent of the tree, walking down from `end`.
pub fn covered_where(&self, start: u64, end: u64, ok: impl Fn(&Entry<O, S>) -> bool) -> bool {
/// it answers `ok`, given where it starts: one descent of the tree,
/// walking down from `end`.
pub fn covered_where(&self, start: u64, end: u64, ok: impl Fn(u64, &Entry<O, S>) -> bool) -> bool {
// `[low, end)` is covered by entries that answered `ok`.
let mut low = end;
let mut below = self.entries.range(..end).rev();
while low > start {
let Some((&s, entry)) = below.next() else { return false };
if entry.end < low || !ok(entry) {
if entry.end < low || !ok(s, entry) {
return false;
}
low = s;
Expand Down Expand Up @@ -574,7 +575,7 @@ mod tests {
let pages = &map[page(start)..page(end)];
assert_eq!(v.covered(start, end), pages.iter().all(Option::is_some), "round {round}: covered");
assert_eq!(
v.covered_where(start, end, |e| e.prot == Prot::ReadWrite),
v.covered_where(start, end, |_, e| e.prot == Prot::ReadWrite),
pages.iter().all(|p| p.is_some_and(|(prot, _)| prot == Prot::ReadWrite)),
"round {round}: covered by writable entries"
);
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/clock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub fn set_counter(boot: u64, mult: u64) {
let stated = STATED_MULT.load(Relaxed);
let at_boot = Line { counter: 0, stamp: 0, mult: if stated != 0 { stated } else { mult } }.nanos(boot);
let page = SharedMemObject::create(crate::mm::charge::boot(), crate::mm::PAGE_SIZE).expect("clock: no frame for the clock page");
let lines = crate::DirectMap::from_phys(page.page(0)).as_mut_ptr::<ClockPage>();
let lines = crate::DirectMap::from_phys(page.page(0).expect("clock: its page is committed whole")).as_mut_ptr::<ClockPage>();
// SAFETY: the page is this object's own, which no address space maps yet
// and none unmaps after: held below for the machine's life. Every field of
// `ClockPage` is an atomic, so a shared reference reads and writes it as
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/inbox/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ struct RingState {
/// One atomic word of one ring header; never `&RingHeader` — see the block above.
fn ring_word(ring: &SharedMemObject, ring_off: u64, field_off: usize) -> &core::sync::atomic::AtomicU32 {
let at = ring_off + field_off as u64;
let word = DirectMap::from_phys(ring.page(at) + at % crate::mm::PAGE_SIZE);
let word = DirectMap::from_phys(ring.page(at).expect("inbox: its ring is committed whole") + at % crate::mm::PAGE_SIZE);
// SAFETY: the offset is in-bounds and 4-aligned, so inside one page of the
// object, which outlives the borrow; `AtomicU32` is sound over memory the
// process also writes.
Expand Down
16 changes: 2 additions & 14 deletions kernel/src/mm/pmm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,21 +136,9 @@ pub fn release_floor(job: &Job) {
/// # Safety
/// Each is a frame [`Frame::into_raw`] gave up, that no leaf on any CPU maps
/// any more and nothing else frees.
pub unsafe fn free_drawn(job: &Job, frames: &[u64]) {
pub unsafe fn free_drawn(job: &Job, frames: impl IntoIterator<Item = u64>) {
let pins = pins();
with(|f| f.free_drawn(job.memory(), frames.iter().copied(), &pins));
}

/// Give back every frame of `frames`, drawn for pages `job` committed, under
/// one hold of the allocator's lock.
pub fn free_all(job: &Job, frames: alloc::boxed::Box<[Frame]>) {
let pins = pins();
with(|f| f.free_drawn(job.memory(), frames.iter().map(|frame| frame.phys), &pins));
// Given up only once the allocator's lock is let go: the list goes back
// to the heap, which may give a page back through it.
for frame in frames.into_vec() {
let _ = frame.into_raw();
}
with(|f| f.free_drawn(job.memory(), frames, &pins));
}

/// The pin counters, published once by [`init`]: where they are, how many,
Expand Down
23 changes: 17 additions & 6 deletions kernel/src/mm/space.rs
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ impl AddressSpace {
crate::sched::futex::revoke_frames(frames);
// SAFETY: frames owned leaves gave up, which no leaf maps on any
// CPU now and no waiter is keyed on.
unsafe { pmm::free_drawn(&self.job, frames) };
unsafe { pmm::free_drawn(&self.job, frames.iter().copied()) };
freed += frames.len() as u64;
});
let inventory = &self.inventory;
Expand Down Expand Up @@ -822,7 +822,7 @@ impl AddressSpace {
Leaf::Empty => match &entry.kind {
Kind::Private { committed: false } => unreachable!("space: a reservation at {page:#x} allows an access"),
Kind::Object { object, offset, .. } => {
let phys = object.page(offset + (page - start));
let phys = object.page(offset + (page - start))?;
leaves.set(page, Leaf::Mapped(Mapping { phys, prot, owned: false, cache: object.cache() }));
phys
}
Expand Down Expand Up @@ -850,10 +850,21 @@ impl AddressSpace {
/// Whether every page of `[start, end)` is in a range that allows
/// `access` and is memory the kernel can copy through the direct map:
/// what a copy the kernel makes for a thread asks first. A device's
/// registers or firmware's framebuffer, an aperture, are no buffer.
/// registers or firmware's framebuffer, an aperture, are no buffer. A
/// page an object maps is one only once its object committed it: a copy
/// into one that is not would be lost, and a fault on it is refused.
pub fn allows(&self, start: u64, end: u64, access: Access) -> bool {
self.inventory
.covered_where(start & !(PAGE_SIZE - 1), end, |entry| entry.prot.allows(access) && entry.backed() && copyable(entry))
let start = start & !(PAGE_SIZE - 1);
self.inventory.covered_where(start, end, |at, entry| {
let committed = || match &entry.kind {
Kind::Object { object, offset, .. } => {
let (lo, hi) = (start.max(at), end.min(entry.end));
object.committed(offset + (lo - at), hi - lo)
}
Kind::Private { .. } | Kind::Copy { .. } => true,
};
entry.prot.allows(access) && entry.backed() && copyable(entry) && committed()
})
}

/// The frame and protection a present leaf maps `va` with, populating
Expand Down Expand Up @@ -956,7 +967,7 @@ impl Drop for AddressSpace {
let mut freed = 0;
self.tables.take_owned(FLOOR, TOP, |frames| {
// SAFETY: the space's own frames, which nothing maps once it drops.
unsafe { pmm::free_drawn(&self.job, frames) };
unsafe { pmm::free_drawn(&self.job, frames.iter().copied()) };
freed += frames.len() as u64;
});
self.usage.freed(freed);
Expand Down
4 changes: 2 additions & 2 deletions kernel/src/object/paged.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ impl Held {
}
// SAFETY: `phys` is a frame this pager drew, out of its index or never
// in it, which nothing maps; a reader's pin defers its reuse.
unsafe { pmm::free_drawn(job, &[phys]) };
unsafe { pmm::free_drawn(job, [phys]) };
}

/// Move each held-aside frame no copy pins any more to the spare ones.
Expand Down Expand Up @@ -948,7 +948,7 @@ impl ZeroHandles for PagerObject {
while let Some(phys) = state.held.spare.pop().or_else(|| state.held.draining.pop()) {
// SAFETY: a frame this pager drew and held aside, which nothing
// maps; a pin defers its free to its last unpin.
unsafe { pmm::free_drawn(job, &[phys]) };
unsafe { pmm::free_drawn(job, [phys]) };
}
let pool = state.held.core.end();
pmm::unpromise(job, pool);
Expand Down
Loading