Skip to main content

strat9_kernel/vfs/
threads_scheme.rs

1//! ThreadsScheme : Plan 9 style `/thread` control files.
2//!
3//! Exposes the userspace thread API as filesystem operations. All lifecycle
4//! work is forwarded to [`crate::process::thread_ops`], the exact same
5//! internals as `SYS_THREAD_CREATE/JOIN/EXIT`, so both entry points share one
6//! implementation (single reap loop, single validation rules).
7//!
8//! # File layout
9//!
10//! ```text
11//! /thread/            directory: TIDs of the calling process' threads
12//!   create    write   32-byte ThreadCreateRequest -> read back TID (u32 LE)
13//!   join/<tid>read    blocks until <tid> exits -> exit code (i32 LE)
14//!                     EINVAL self-join, ENOENT absent/already-joined
15//!                     (=> a second open of join/<tid> fails; documented)
16//!   exit      write   exit code (i32 LE); kills current thread, no return
17//!   current   read    caller's TID (u32 LE), resolved at read time
18//!   yield     read    sched_yield; fd shareable across threads
19//!   kill/<tid>open    terminates <tid>; ESRCH absent, EPERM cross-process
20//!   stats     read    kernel-owned stack counters ("allocated N", "active M")
21//! ```
22
23use crate::{
24    process::{current_task_clone, get_all_tasks, thread_ops},
25    sync::SpinLock,
26    syscall::error::SyscallError,
27};
28use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec};
29use core::sync::atomic::{AtomicU64, Ordering};
30
31/// Operation encoded in the high bits of a file_id.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33enum Op {
34    Root = 0,
35    Create = 1,
36    Join = 2,
37    Exit = 3,
38    Current = 4,
39    Yield = 5,
40    Kill = 6,
41    Stats = 7,
42}
43
44/// Per-fd state retained between open/write/read.
45#[derive(Clone)]
46enum OpState {
47    Root,
48    /// Remembers the TID produced by write() so read() can hand it back.
49    Create {
50        created_tid: Option<u32>,
51    },
52    Join {
53        target_tid: u32,
54    },
55    Static(Op),
56}
57
58fn op_of(state: &OpState) -> Op {
59    match state {
60        OpState::Root => Op::Root,
61        OpState::Create { .. } => Op::Create,
62        OpState::Join { .. } => Op::Join,
63        OpState::Static(op) => *op,
64    }
65}
66
67/// Binary protocol for `/thread/create` writes.
68///
69/// ```text
70/// #[repr(C)] struct ThreadCreateRequest {
71///     entry: u64,       // user trampoline address (< USER_TOP)
72///     stack_size: u64,  // requested size; kernel allocates and places RSP
73///     arg0: u64,        // passed in RDI (e.g. closure pointer)
74///     tls_base: u64,    // FS.base (0 = none)
75/// }
76/// // write(fd, &req, 32) then read(fd, &tid, 4)
77/// ```
78const THREAD_CREATE_REQ_LEN: usize = 32;
79
80pub struct ThreadsScheme {
81    states: SpinLock<BTreeMap<u64, OpState>>,
82}
83
84impl ThreadsScheme {
85    /// Creates a new instance.
86    pub fn new() -> Self {
87        ThreadsScheme {
88            states: SpinLock::new(BTreeMap::new()),
89        }
90    }
91
92    fn alloc_file_id(op: Op) -> u64 {
93        static NEXT_SEQ: AtomicU64 = AtomicU64::new(1);
94        let seq = NEXT_SEQ.fetch_add(1, Ordering::Relaxed);
95        ((op as u64) << 32) | (seq & 0xFFFF_FFFF)
96    }
97
98    fn state_for(&self, file_id: u64) -> Result<(Op, OpState), SyscallError> {
99        let states = self.states.lock();
100        let st = states
101            .get(&file_id)
102            .cloned()
103            .ok_or(SyscallError::BadHandle)?;
104        drop(states);
105        Ok((op_of(&st), st))
106    }
107
108    fn take_state(&self, file_id: u64) -> Result<OpState, SyscallError> {
109        self.states
110            .lock()
111            .remove(&file_id)
112            .ok_or(SyscallError::BadHandle)
113    }
114
115    /// Parse `join/<tid>` and validate kinship synchronously so errors surface
116    /// at open time (self-join => EINVAL, missing/reaped child => ENOENT).
117    ///
118    /// The blocking reap itself happens on read(), via [`thread_ops::join_task`].
119    fn parse_join(path: &str) -> Result<u32, SyscallError> {
120        let tid_str = path.strip_prefix("join/").ok_or(SyscallError::NotFound)?;
121        if tid_str.is_empty() || !tid_str.bytes().all(|b| b.is_ascii_digit()) {
122            return Err(SyscallError::InvalidArgument);
123        }
124        let target_tid: u32 = tid_str.parse().map_err(|_| SyscallError::InvalidArgument)?;
125
126        let cur = current_task_clone().ok_or(SyscallError::Fault)?;
127        if target_tid == cur.tid {
128            return Err(SyscallError::InvalidArgument);
129        }
130        use crate::process::{current_task_id, get_child_task_id_by_tid};
131        let parent_id = current_task_id().ok_or(SyscallError::Fault)?;
132        get_child_task_id_by_tid(parent_id, target_tid).ok_or(SyscallError::NotFound)?;
133        Ok(target_tid)
134    }
135
136    /// Parse `kill/<tid>`; the kill itself is performed by open() (Plan 9
137    /// style: opening the control file performs the action). Errors:
138    /// ESRCH unknown tid, EPERM other thread group.
139    fn handle_kill_open(path: &str) -> Result<(), SyscallError> {
140        let tid_str = path.strip_prefix("kill/").ok_or(SyscallError::NotFound)?;
141        if tid_str.is_empty() || !tid_str.bytes().all(|b| b.is_ascii_digit()) {
142            return Err(SyscallError::InvalidArgument);
143        }
144        let target_tid: u32 = tid_str.parse().map_err(|_| SyscallError::InvalidArgument)?;
145        let cur = current_task_clone().ok_or(SyscallError::Fault)?;
146        thread_ops::kill_thread(cur.tid, target_tid)
147    }
148
149    /// Directory listing of the calling process' threads (pattern procfs).
150    fn root_listing(&self) -> Result<String, SyscallError> {
151        let cur = current_task_clone().ok_or(SyscallError::Fault)?;
152        let mut out = String::new();
153        if let Some(tasks) = get_all_tasks() {
154            for t in tasks {
155                if t.tgid == cur.tgid {
156                    out.push_str(itoa_u32(t.tid).as_str());
157                    out.push('\n');
158                }
159            }
160        }
161        Ok(out)
162    }
163
164    /// Handle a write to `/thread/create`: parse the request, build the task
165    /// with a kernel-owned stack, register it, and remember its TID.
166    fn handle_create_write(&self, buf: &[u8]) -> Result<u32, SyscallError> {
167        if buf.len() != THREAD_CREATE_REQ_LEN {
168            return Err(SyscallError::InvalidArgument);
169        }
170        let req_le = |i: usize| {
171            let mut raw = [0u8; 8];
172            raw.copy_from_slice(&buf[i..i + 8]);
173            u64::from_le_bytes(raw)
174        };
175        let entry = req_le(0);
176        let stack_size = req_le(8);
177        let arg0 = req_le(16);
178        let tls_base = req_le(24);
179
180        let child = thread_ops::create_user_thread_with_kernel_stack(
181            thread_ops::UserEntryContext::ring3(),
182            entry,
183            stack_size,
184            arg0,
185            tls_base,
186        )?;
187        Ok(child.tid)
188    }
189}
190
191impl Default for ThreadsScheme {
192    fn default() -> Self {
193        Self::new()
194    }
195}
196
197/// Minimal decimal formatter (no formatting machinery in hot paths).
198fn itoa_u32(mut v: u32) -> String {
199    if v == 0 {
200        return String::from("0");
201    }
202    let mut buf = [0u8; 10];
203    let mut i = buf.len();
204    while v > 0 {
205        i -= 1;
206        buf[i] = b'0' + (v % 10) as u8;
207        v /= 10;
208    }
209    // SAFETY: buffer content is ASCII digits by construction.
210    String::from(unsafe { core::str::from_utf8_unchecked(&buf[i..]) })
211}
212
213use crate::vfs::scheme::{
214    finalize_pseudo_stat, DirEntry, FileFlags, FileStat, OpenFlags, OpenResult, Scheme,
215    DEV_THREADFS, DT_DIR, DT_REG,
216};
217
218impl Scheme for ThreadsScheme {
219    fn open(&self, path: &str, _flags: OpenFlags) -> Result<OpenResult, SyscallError> {
220        // Normalize: strip leading '/' (mount layer already stripped prefix).
221        let path = path.trim_start_matches('/');
222
223        let (op, state) = match path {
224            "" | "/" => (Op::Root, OpState::Root),
225            "create" => (Op::Create, OpState::Create { created_tid: None }),
226            "exit" => (Op::Exit, OpState::Static(Op::Exit)),
227            "current" => (Op::Current, OpState::Static(Op::Current)),
228            "yield" => (Op::Yield, OpState::Static(Op::Yield)),
229            "stats" => (Op::Stats, OpState::Static(Op::Stats)),
230            p if p.starts_with("join/") => {
231                let target = Self::parse_join(p)?;
232                (Op::Join, OpState::Join { target_tid: target })
233            }
234            p if p.starts_with("kill/") => {
235                Self::handle_kill_open(p)?;
236                (Op::Kill, OpState::Static(Op::Kill))
237            }
238            _ => return Err(SyscallError::NotFound),
239        };
240
241        let file_id = Self::alloc_file_id(op);
242        self.states.lock().insert(file_id, state);
243
244        let flags = if op == Op::Root {
245            FileFlags::DIRECTORY
246        } else {
247            FileFlags::empty()
248        };
249        Ok(OpenResult {
250            file_id,
251            size: None,
252            flags,
253        })
254    }
255
256    fn read(&self, file_id: u64, offset: u64, buf: &mut [u8]) -> Result<usize, SyscallError> {
257        if buf.is_empty() {
258            return Ok(0);
259        }
260        let (_, state) = self.state_for(file_id)?;
261
262        match state {
263            OpState::Root => {
264                let content = self.root_listing()?;
265                serve_string(&content, offset, buf)
266            }
267            OpState::Create { created_tid } => {
268                let Some(tid) = created_tid else {
269                    // read before write: protocol violation.
270                    return Err(SyscallError::InvalidArgument);
271                };
272                if offset != 0 {
273                    return Ok(0);
274                }
275                let bytes = tid.to_le_bytes();
276                let n = bytes.len().min(buf.len());
277                buf[..n].copy_from_slice(&bytes[..n]);
278                Ok(n)
279            }
280            OpState::Join { target_tid } => {
281                // Blocking reap loop shared with SYS_THREAD_JOIN.
282                let (_tid, status) = thread_ops::join_task(target_tid)?;
283                if offset != 0 {
284                    return Ok(0);
285                }
286                let bytes = status.to_le_bytes();
287                let n = bytes.len().min(buf.len());
288                buf[..n].copy_from_slice(&bytes[..n]);
289                Ok(n)
290            }
291            OpState::Static(op) => match op {
292                Op::Current => {
293                    let tid = thread_ops::current_thread_tid()?;
294                    if offset != 0 {
295                        return Ok(0);
296                    }
297                    let bytes = tid.to_le_bytes();
298                    let n = bytes.len().min(buf.len());
299                    buf[..n].copy_from_slice(&bytes[..n]);
300                    Ok(n)
301                }
302                Op::Yield => {
303                    crate::process::yield_task();
304                    Ok(0)
305                }
306                Op::Kill => Ok(0),
307                Op::Stats => {
308                    let (allocated, active) = thread_ops::kernel_user_stack_stats();
309                    let mut content = String::from("allocated ");
310                    content.push_str(itoa_u32(allocated as u32).as_str());
311                    content.push('\n');
312                    content.push_str("active ");
313                    content.push_str(itoa_u32(active as u32).as_str());
314                    content.push('\n');
315                    serve_string(&content, offset, buf)
316                }
317                _ => Err(SyscallError::PermissionDenied),
318            },
319        }
320    }
321
322    fn write(&self, file_id: u64, _offset: u64, buf: &[u8]) -> Result<usize, SyscallError> {
323        let (op, _) = self.state_for(file_id)?;
324        match op {
325            Op::Create => {
326                let tid = self.handle_create_write(buf)?;
327                if let Some(OpState::Create { created_tid }) = self.states.lock().get_mut(&file_id)
328                {
329                    *created_tid = Some(tid);
330                } else {
331                    return Err(SyscallError::BadHandle);
332                }
333                Ok(THREAD_CREATE_REQ_LEN)
334            }
335            Op::Exit => {
336                if buf.len() < 4 {
337                    return Err(SyscallError::InvalidArgument);
338                }
339                let code = i32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
340                // Never returns.
341                thread_ops::exit_current_thread(code)
342            }
343            _ => Err(SyscallError::PermissionDenied),
344        }
345    }
346
347    fn close(&self, file_id: u64) -> Result<(), SyscallError> {
348        self.take_state(file_id)?;
349        Ok(())
350    }
351
352    fn stat(&self, file_id: u64) -> Result<FileStat, SyscallError> {
353        let op = (file_id >> 32) as u64;
354        let is_dir = op == Op::Root as u64;
355        let st = if is_dir {
356            FileStat {
357                st_ino: file_id,
358                st_mode: 0o040555,
359                st_nlink: 2,
360                ..FileStat::zeroed()
361            }
362        } else {
363            FileStat {
364                st_ino: file_id,
365                st_mode: 0o100666,
366                st_nlink: 1,
367                ..FileStat::zeroed()
368            }
369        };
370        Ok(finalize_pseudo_stat(st, DEV_THREADFS, 0))
371    }
372
373    fn readdir(&self, _file_id: u64) -> Result<Vec<DirEntry>, SyscallError> {
374        Ok(alloc::vec![
375            DirEntry {
376                ino: Self::alloc_file_id(Op::Create),
377                file_type: DT_REG,
378                name: String::from("create"),
379            },
380            DirEntry {
381                ino: Self::alloc_file_id(Op::Join),
382                file_type: DT_DIR,
383                name: String::from("join"),
384            },
385            DirEntry {
386                ino: Self::alloc_file_id(Op::Exit),
387                file_type: DT_REG,
388                name: String::from("exit"),
389            },
390            DirEntry {
391                ino: Self::alloc_file_id(Op::Current),
392                file_type: DT_REG,
393                name: String::from("current"),
394            },
395            DirEntry {
396                ino: Self::alloc_file_id(Op::Yield),
397                file_type: DT_REG,
398                name: String::from("yield"),
399            },
400            DirEntry {
401                ino: Self::alloc_file_id(Op::Kill),
402                file_type: DT_DIR,
403                name: String::from("kill"),
404            },
405            DirEntry {
406                ino: Self::alloc_file_id(Op::Stats),
407                file_type: DT_REG,
408                name: String::from("stats"),
409            },
410        ])
411    }
412}
413
414/// Serve string content honoring read offsets (procfs pattern).
415fn serve_string(content: &str, offset: u64, buf: &mut [u8]) -> Result<usize, SyscallError> {
416    if offset >= content.len() as u64 {
417        return Ok(0);
418    }
419    let start = offset as usize;
420    let end = core::cmp::min(start + buf.len(), content.len());
421    buf[..end - start].copy_from_slice(&content.as_bytes()[start..end]);
422    Ok(end - start)
423}
424
425/// Convenience constructor matching the other scheme registration sites.
426pub fn init_threads_scheme() -> Arc<ThreadsScheme> {
427    Arc::new(ThreadsScheme::new())
428}