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
27 changes: 27 additions & 0 deletions issues/two-holders-racing-on-an-exported-socket-can-both-stall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
status: open
kind: defect
opened: 2026-10-24
---

# Two holders racing on an exported socket can both stall

`netstack_holders_exported_to_children`'s last step (`tests/guest/rust-tests/src/bin/socket_export.rs`,
`race`) has the parent and a second child each block on a listener and a
datagram socket they both hold, while the harness connects twice and sends two
datagrams. Now and then neither process goes on: the racer is spawned
(`spawn: /system/bin/test_rs_socket_export`), the first part's line is printed,
and the guest says nothing more until the harness's ceiling (952–970 s guards
expired).

QEMU x86-64 under TCG on an aarch64 Mac, the test alone, `--nocapture`: one
stall in six runs at `32046264b` (main), two in six at PR #1060's head, whose
change touches no socket or netstack code. Each stalled run's last guest lines
are the racer's spawn and netstack's mDNS line. Logs: PR #1060's evidence,
`qemu/netstack-*`.

What blocks is not established: no guest line says whether either racer sent
its `racing` datagram, which the harness waits for before it connects.

**Exit**: the race finishes in every run of a repeated QEMU loop, with the
blocked step named by a line the guest prints, or the cause found and fixed.
72 changes: 0 additions & 72 deletions lib/bcachefs/src/crc32c.rs

This file was deleted.

26 changes: 26 additions & 0 deletions lib/bcachefs/src/crc32c/aarch64.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! FEAT_CRC32's `CRC32CX` and `CRC32CB`, which accumulate CRC-32C (Arm ARM,
//! DDI 0487, "CRC32C"; mandatory from Armv8.1, ID_AA64ISAR0_EL1.CRC32).
//! Every AArch64 machine ToyOS supports has it, and so do the hosts that
//! build and test it.

use core::arch::aarch64::{__crc32cb, __crc32cd};

pub fn update(crc: u32, data: &[u8]) -> u32 {
// SAFETY: the CPU has FEAT_CRC32 (the module header), which is all
// `accumulate` asks of its caller.
unsafe { accumulate(crc, data) }
}

/// # Safety
/// The CPU has FEAT_CRC32.
#[target_feature(enable = "crc")]
unsafe fn accumulate(mut crc: u32, data: &[u8]) -> u32 {
let (words, tail) = data.as_chunks::<8>();
for word in words {
crc = __crc32cd(crc, u64::from_le_bytes(*word));
}
for &byte in tail {
crc = __crc32cb(crc, byte);
}
crc
}
95 changes: 95 additions & 0 deletions lib/bcachefs/src/crc32c/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! CRC-32C (Castagnoli, polynomial 0x1EDC6F41), by the CPU's own instruction:
//! SSE4.2's `CRC32` on x86-64, FEAT_CRC32's `CRC32C` on AArch64. Every
//! machine ToyOS runs on and every host that builds or tests it has one, so
//! nothing computes it any other way.

#[cfg_attr(target_arch = "x86_64", path = "x86_64.rs")]
#[cfg_attr(target_arch = "aarch64", path = "aarch64.rs")]
mod arch;

#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
compile_error!("bcachefs computes CRC-32C with the CPU's instruction, and names one for x86-64 and AArch64 alone");

/// Compute CRC-32c over a byte slice.
pub fn crc32c(data: &[u8]) -> u32 {
arch::update(0xFFFF_FFFF, data) ^ 0xFFFF_FFFF
}

/// The same register with no initial or final inversion: bcachefs's data
/// checksum type 5, whose value over zeros is 0.
pub fn crc32c_zero(data: &[u8]) -> u32 {
arch::update(0, data)
}

#[cfg(test)]
mod tests {
use super::*;

/// The oracle: the reflected polynomial a bit at a time, sharing nothing
/// with the instruction but the register's definition.
fn reference(mut crc: u32, data: &[u8]) -> u32 {
for &byte in data {
crc ^= u32::from(byte);
for _ in 0..8 {
crc = if crc & 1 != 0 { (crc >> 1) ^ 0x82F6_3B78 } else { crc >> 1 };
}
}
crc
}

/// RFC 3720's own examples (Appendix B.4), and the catalogue's check
/// value over "123456789".
#[test]
fn rfc_3720_vectors() {
let ascending: Vec<u8> = (0..32).collect();
let descending: Vec<u8> = (0..32).rev().collect();
#[rustfmt::skip]
let read_pdu: [u8; 48] = [
0x01, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x18,
0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let vectors: [(&[u8], u32); 6] = [
(&[0; 32], 0x8A91_36AA),
(&[0xFF; 32], 0x62A8_AB43),
(&ascending, 0x46DD_794E),
(&descending, 0x113F_DB5C),
(&read_pdu, 0xD996_3A56),
(b"123456789", 0xE306_9283),
];
for (data, want) in vectors {
assert_eq!(crc32c(data), want, "{data:02x?}");
assert_eq!(reference(!0, data) ^ !0, want, "the oracle on {data:02x?}");
}
assert_eq!(crc32c(b""), 0);
}

#[test]
fn the_uninverted_register_has_its_own_check_value() {
assert_eq!(crc32c_zero(b"123456789"), 0x58E3_FA20);
assert_eq!(crc32c_zero(&[0; 4096]), 0);
}

/// Every length to past two words, at every alignment a word can have,
/// and whole nodes, against the oracle: the instruction's word steps and
/// its byte tail agree with a bit at a time.
#[test]
fn the_instruction_is_the_oracle_at_every_length_and_alignment() {
let mut state = 0x9E37_79B9_7F4A_7C15u64;
let bytes: Vec<u8> = (0..16_384 + 8)
.map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state as u8
})
.collect();
for at in 0..8 {
for len in (0..=24).chain([4088, 4096, 16_384]) {
let data = &bytes[at..at + len];
assert_eq!(crc32c(data), reference(!0, data) ^ !0, "{len} bytes at {at}");
assert_eq!(crc32c_zero(data), reference(0, data), "{len} bytes at {at}, uninverted");
}
}
}
}
29 changes: 29 additions & 0 deletions lib/bcachefs/src/crc32c/x86_64.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! SSE4.2's `CRC32`, which accumulates CRC-32C (Intel SDM Vol. 2A, "CRC32 —
//! Accumulate CRC32 Value"; CPUID.01H:ECX.SSE4_2[bit 20]). Every x86-64 part
//! ToyOS supports, 2020 and later, has it, and so do the hosts that build and
//! test it.

use core::arch::x86_64::{_mm_crc32_u64, _mm_crc32_u8};

pub fn update(crc: u32, data: &[u8]) -> u32 {
// SAFETY: the CPU has SSE4.2 (the module header), which is all
// `accumulate` asks of its caller.
unsafe { accumulate(crc, data) }
}

/// # Safety
/// The CPU has SSE4.2.
#[target_feature(enable = "sse4.2")]
unsafe fn accumulate(crc: u32, data: &[u8]) -> u32 {
let (words, tail) = data.as_chunks::<8>();
let mut wide = u64::from(crc);
for word in words {
wide = _mm_crc32_u64(wide, u64::from_le_bytes(*word));
}
// The instruction's 64-bit form leaves the register in the low half.
let mut crc = wide as u32;
for &byte in tail {
crc = _mm_crc32_u8(crc, byte);
}
crc
}
21 changes: 15 additions & 6 deletions lib/bcachefs/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ fn push_extent(extents: &mut Vec<Extent>, start: u64, count: u32) {
extents.push(Extent { start_block: start, block_count: count, _reserved: 0 });
}

/// What an entry records besides its name, as [`Mounted::list`] answers it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Recorded {
pub symlink: bool,
pub size: u64,
pub mtime: u64,
}

/// Filesystem error type with rich context.
#[derive(Debug)]
pub enum FsError {
Expand Down Expand Up @@ -706,11 +714,11 @@ impl<IO: BlockIO, Mode> Mounted<IO, Mode> {
}
}

/// Up to `limit` of the files `keep` accepts, as (name, size); more is
/// refused before any materialise. `limit` counts the kept names, so a
/// caller listing one directory pays that directory's bound and not the
/// volume's.
pub fn list(&self, limit: usize, keep: &dyn Fn(&str) -> bool) -> Result<Vec<(String, u64)>, FsError> {
/// Up to `limit` of the files `keep` accepts, with what each one's entry
/// records; more is refused before any materialise. `limit` counts the
/// kept names, so a caller listing one directory pays that directory's
/// bound and not the volume's.
pub fn list(&self, limit: usize, keep: &dyn Fn(&str) -> bool) -> Result<Vec<(String, Recorded)>, FsError> {
let mut result = Vec::new();
let ControlFlow::Continue(()) = btree::for_each_live(&self.io, self.sb.root_node, &mut |entry| {
let Ok(leaf) = self.decode(&entry.value) else { return Ok(ControlFlow::Continue(())) };
Expand All @@ -720,7 +728,8 @@ impl<IO: BlockIO, Mode> Mounted<IO, Mode> {
if result.len() >= limit {
return Err(FsError::ListTooLong { limit });
}
result.push((String::from(leaf.name()), leaf.size()));
let symlink = matches!(leaf, LeafValue::Symlink { .. });
result.push((String::from(leaf.name()), Recorded { symlink, size: leaf.size(), mtime: leaf.mtime() }));
Ok(ControlFlow::Continue(()))
})? else {
unreachable!("`list`'s visitor never breaks")
Expand Down
2 changes: 1 addition & 1 deletion lib/bcachefs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub mod engine;
pub use block_io::{BlockIO, BlockBuf, BlockNum, DeviceError, TransferError};
#[cfg(feature = "std")]
pub use block_io::VecBlockIO;
pub use fs::{file_entry_fits, Formatted, Mounted, ReadOnly, ReadWrite, FsError, Extent};
pub use fs::{file_entry_fits, Formatted, Mounted, ReadOnly, ReadWrite, Recorded, FsError, Extent};
pub use superblock::{DESIGNATION_BLOCKS_OFFSET, DESIGNATION_MAGIC, FsUuid, Superblock};

/// Records the largest single allocation each test thread makes, so a test can
Expand Down
4 changes: 2 additions & 2 deletions lib/bcachefs/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fn create_single_small_file() {
let files = mounted.list(usize::MAX, &|_| true).expect("list failed");
assert_eq!(files.len(), 1);
assert_eq!(files[0].0, "hello.txt");
assert_eq!(files[0].1, 13);
assert_eq!(files[0].1, bcachefs::Recorded { symlink: false, size: 13, mtime: 42 });

let data = mounted.read_file("hello.txt").expect("read failed");
assert_eq!(data, b"Hello, world!");
Expand Down Expand Up @@ -134,7 +134,7 @@ fn empty_file() {
let files = mounted.list(usize::MAX, &|_| true).expect("list");
assert_eq!(files.len(), 1);
assert_eq!(files[0].0, "empty");
assert_eq!(files[0].1, 0);
assert_eq!(files[0].1.size, 0);

let data = mounted.read_file("empty").expect("read empty");
assert!(data.is_empty());
Expand Down
18 changes: 14 additions & 4 deletions sdk/toyos/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@
//! **A request carries a tag and its span of the window.** The client lends
//! the server a [`WINDOW_BYTES`] region at connect ([`HELLO`]); a request's
//! paths, data and answer travel in the span it names, and the frames stay
//! small and fixed. The reply carries the request's tag. A client keeps at
//! most [`MAX_OUTSTANDING`] requests unanswered on one connection and lets no
//! two of their spans overlap; a server reads a request's input out of its
//! small and fixed. The reply carries the request's tag; a [`CLOSE`] has
//! none, so a close costs its client a send and no wait and is never
//! outstanding. A client keeps at most [`MAX_OUTSTANDING`] requests
//! unanswered on one connection and lets no two of their spans overlap; a
//! server reads a request's input out of its
//! span before it writes its answer there. The window is shared memory both
//! sides write, so each side copies what it reads out of it once and acts on
//! the copy.
Expand Down Expand Up @@ -93,6 +95,8 @@ pub const OPEN: u32 = 2;
/// A file with no name, in the directory the path names, opened by `flags` as
/// [`OPEN`] opens: a later [`LINK`] gives it one, and nothing else ever does.
pub const CREATE_UNNAMED: u32 = 3;
/// Let the id `fid` go. Answered with no frame: a close the volume refuses
/// keeps its node for the next sync, which the server says in its log.
pub const CLOSE: u32 = 4;
/// Read the span's length at the description's offset into the span, and move
/// the offset past it: `value` answers how much.
Expand Down Expand Up @@ -927,8 +931,14 @@ impl Dir {
Ok(Opened { fid, stat: stat_of(&reply) })
}

/// Let `fid` go, without waiting for the server: [`CLOSE`] is answered
/// with nothing.
pub fn close(&mut self, fid: Fid) {
let _ = self.fid_call(CLOSE, fid, Request::new(), 0);
let Ok(id) = self.held(fid) else { return };
let conn = self.conn.as_ref().expect("an id is held on a connection");
if conn.send(CLOSE, &Request { fid: id, ..Request::new() }).is_err() {
self.conn = None;
}
}

/// Read at most `out.len()` bytes, and at most the window, at the
Expand Down
Loading