The Object Manager (Ob) is the central abstraction for syscalls, handles,
security, and namespace. It unifies what were previously separate subsystems
(handles, KOBJ, URN, security) into a single kernel object graph. Every
kernel-visible resource is an ObObject, accessible via path in the Ob
namespace or by handle in a process's handle table.
pub struct ObObjectTable {
slots: Vec<Option<ObObject>>,
count: usize,
next_id: ObId,
}The global object table is protected by Mutex<ObObjectTable>. It stores all
live kernel objects. Objects are assigned an ObId (u64) on creation and
looked up by ID.
pub struct ObObject {
pub id: ObId,
pub obj_type: ObType,
pub name: [u8; OB_NAME_LEN], // 128 bytes
pub refcount: u32,
pub flags: u32,
pub native_id: u64,
pub ops: Option<&'static dyn ObOperations>,
}id: unique 64-bit identifier, monotonically increasing from 1obj_type: discriminates object semantics (Process, Pipe, Event, etc.)name: 128-byte zero-terminated string, human-readablerefcount: number of outstanding references (handles + namespace entries)native_id: opaque type-specific data (inode for files, PID for processes)flags: type-specific flags (e.g., drive index for file handles)ops: optional vtable for polymorphic behavior
pub trait ObOperations: Send + Sync {
fn on_destroy(&self, _id: ObId, _native_id: u64) {}
}Currently provides on_destroy() called when the object is removed. This
enables type-specific cleanup (e.g., pipe buffer teardown, semaphore wake).
The FileHandleOps struct is a no-op implementation used by VFS handles.
pub struct HandleEntry {
pub object_id: ObId,
pub offset: u64, // per-handle file offset
}Special sentinel IDs: HANDLE_CLOSED=0, HANDLE_STDIN=ObId::MAX,
HANDLE_STDOUT=ObId::MAX-1, HANDLE_STDERR=ObId::MAX-2.
Each process's handle table (HandleTable) is a Vec<HandleEntry> with fds
0=stdin, 1=stdout, 2=stderr pre-allocated. alloc_handle() finds the first
free slot >= 3. The ob_object() variant also stores desired_access in
offset for SeAccessCheck re-verification.
| Value | Type | Description |
|---|---|---|
| 0 | Unknown | Uninitialized/error |
| 1 | Process | Process object (waitable on ChildExit) |
| 2 | Driver | NEM driver object |
| 3 | Device | Hardware device object |
| 4 | Pipe | Unidirectional data pipe |
| 5 | EventBus | Kernel event bus (kernel only) |
| 6 | BlockDevice | Block storage device (kernel) |
| 7 | Filesystem | Filesystem instance (internal) |
| 8 | MemoryRegion | Physical memory region (kernel) |
| 9 | Symlink | Namespace symbolic link |
| 10 | MountPoint | Filesystem mount point (kernel) |
| 11 | Directory | Namespace directory |
| 12 | Key | Registry key (Cm) |
| 13 | Event | Manual/auto-reset event (waitable) |
| 14 | Semaphore | Counting semaphore (waitable) |
| 15 | Timer | One-shot or periodic timer (waitable) |
| 16 | Thread | Thread object (waitable on join) |
| 17 | Section | Shared memory section (maps to VMA) |
| 18 | Socket | Network socket (Tcp/Udp) |
| 19 | Session | Login session (USR-P1a; lifecycle/tokens in USR-P2) |
| 20 | Service | Managed service process (Sm) |
| 21 | PowerManager | Power management (kernel-created, \System\PowerManager) |
| 22 | KeyboardDevice | Keyboard device — NeoKBD (kernel-created, \Device\Keyboard) |
\ (root)
├── \Global\
│ ├── \Global\Info\ — virtual read-only objects
│ │ ├── CpuInfo — ob_query_info(class=7); CpuStats (class=24)
│ │ ├── Threads — ob_query_info(class=25) all-thread snapshot
│ │ ├── DateTime — RTC date/time (query class=9; set class=50 via ob_set_info)
│ │ ├── Memory — physical + kernel heap stats (class=10)
│ │ ├── Version — kernel version string (class=8)
│ │ ├── Cwd — current working directory per process
│ │ ├── Keyboard — keyboard layout (set via ob_set_info, class=5)
│ │ ├── Drives — mounted drive list (class=11)
│ │ ├── Drivers — NEM driver registry (class=12)
│ │ └── VtInfo — VT information (class=11 sub)
│ ├── \Global\FileSystem\C:\ — VFS mount point for NeoDOS FS
│ └── \Global\FileSystem\A:\ — VFS mount point for FAT32 ESP
├── \Device\ — device objects
│ ├── Tcp — TCP network device
│ ├── Udp — UDP network device
│ ├── Harddisk0 — primary block device
│ ├── NeoDosVolume0 — NeoDOS FS volume
│ ├── EspVolume0 — ESP FAT32 volume
│ ├── Keyboard — keyboard device (NeoKBD, ObType::KeyboardDevice)
│ └── ...
├── \Driver\ — NEM driver objects
│ ├── Ahci — AHCI NEM driver
│ ├── E1000 — e1000 NIC driver
│ └── ...
├── \System\ — system objects
│ └── PowerManager — power management (ObType::PowerManager, selo shutdown/reboot via ob_set_info)
├── \Registry\ — registry keys (Cm)
├── \Service\ — registered service objects (ObType::Service)
├── \Ob\Process\ — PID-indexed process objects
├── \Security\ — security objects (future)
└── \DosDevices\ — drive letter symlinks (C:, A:)
- Resolve path via
ob_lookup_path()(symlink traversal, case-insensitive) - Perform
SeAccessCheckagainst the object's DACL using the caller's token - Allocate a
HandleEntryin the process handle table withob_object(ob_id, desired_access) - Return fd (>=3) or error
- Validate ObType — only user-creatable types: Process(1), Driver(2), Pipe(4), Directory(11), Event(13), Semaphore(14), Timer(15), Thread(16), Section(17), Service(20). PowerManager(21) and KeyboardDevice(22) are kernel-created only.
- Call
ob_create_object()in the global table with given name + type - Insert into namespace at the given path
- Allocate handle entries for any returned fds (pipe creates bidirectional pair)
- Return fd(s)
Process creation is shared (#375).
ObCreate(Process)and the kernel Service Manager both call the same internal helperusermode::create_process_from_ob_path(VFS read → user slot → ELF load →spawn_usermode); there is no second ELF-load/spawn implementation inservices/manager.rs. The new thread is leftSuspendedand is publishedReadyby the single activation pathScheduler::activate_suspended_process, called from theObWaithand-off (syscall/ob/wait.rs) and, for services, viausermode::activate_process.
Supports the following info classes:
| Class | Name | Description |
|---|---|---|
| 0 | Basic | Object type + refcount |
| 1 | Name | Object name string |
| 2 | File | File size + attributes (VFS) |
| 3 | Process | PID + parent PID + priority + thread count + aggregated state |
| 4 | Thread | TID + PID + state + priority of the first matching KTHREAD (see 25 for all threads) |
| 5 | Pipe | Pipe buffer size + available |
| 6 | Device | Device type + status |
| 7 | CpuInfo | CpuInfoFull for the calling CPU (identity, features, address widths) + total cpu_count |
| 8 | Version | Kernel version string |
| 9 | DateTime | RTC date/time |
| 10 | Memory | Phys total/usable/free + heap stats |
| 11 | Drives | Mounted drive list |
| 12 | Drivers | NEM driver list + states |
| 13 | Cwd | Current working directory |
| 14 | KeyboardLayout | Current layout ID |
| 15 | ReadContent | File read at handle offset |
| 16 | VolumeLabel | Volume label (get) |
| 17 | SocketInfo | Socket type + state |
| 18 | SocketAddr | Bound/peer address |
| 19 | TcpStatus | TCP connection state |
| 20 | NicInfo | NIC hardware info (MAC, IP, link, vendor/device IDs, name, description) |
| 21 | RegistryKey | Key metadata |
| 22 | RegistryValue | Value data |
| 23 | SocketRecv | Receive from socket |
| 24 | CpuStats | Per-CPU snapshot [StatsHeader][CpuStatsEntry] — \Global\Info\CpuInfo |
| 25 | ThreadStats | All-thread snapshot [StatsHeader][ThreadStatsEntry] — \Global\Info\Threads |
| 26 | ProcessSnapshot | Coherent process+thread snapshot [ProcSnapshotHeader][ProcessInfoRaw][ThreadInfoRaw] — \Global\Info\Processes |
| 27 | SmpStats | Global work-stealing counters SmpStats (24 bytes) — \Global\Info\CpuInfo (Fase 3 M3.2) |
| 28 | NetStats | Per-interface counters (40 bytes/entry: rx/tx packets/bytes, rx/tx errors) in NicInfo enumeration order — #373 |
| 29 | ServiceState | Service state (state+pid+uptime) |
| 30 | ServiceConfig | Service configuration (start type, restart policy, max failures) |
| 31 | ServiceStatus | Comprehensive status (state+pid+exit count+exit code+failures+uptime) |
| 33 | FsckStatus | FsckStatsRaw — filesystem integrity check results (read-only) |
| 34 | ProcessId | u32 LE — current process PID |
| 35 | KeyboardInfo | KbdState (modifiers, leds, active_layout_index) — \Device\Keyboard |
| 36 | KeyboardCaps | KbdCaps (max_layouts, capabilities, num_layouts) — \Device\Keyboard |
| 37 | KeyboardLayouts | [KbdLayoutInfo] — list of loaded layouts on \Device\Keyboard |
| 38 | Hostname | System hostname string (null-terminated) — any Ob object, reads from Registry |
| 39 | ProcessArgs | Per-process command-line args (256 bytes, null-terminated) — any valid handle, returns current process args copied atomically at sys_ob_create(PROCESS) from 0x41F000 (fixes 0x41F000 data race in pipelines) |
| 40 | TimeZone | SysTimeZone (16 bytes: standard/DST offsets + DST window) read from the Registry — \Global\Info\DateTime (#357) |
| 41 | LocalDateTime | SysDateTime (7 bytes) with the RTC UTC time converted to local using the configured timezone — \Global\Info\DateTime (#357) |
| 42 | ProcessShutdownState | 1 byte: 0 = normal execution, 1 = graceful shutdown requested. Any valid handle; answers for the calling process. Set by the Service Manager stop_service() and cleared on process exit — #358 |
| 32 | PowerState | PowerSystemState u32 (Active/ShuttingDown/Rebooting/Suspending/Hibernating/Off) — \System\PowerManager. Class declared; handler not yet implemented. |
Additive ABI v8 extension: no existing class, struct, or syscall number changed. Both classes return a size-negotiated snapshot:
buffer = [StatsHeader][Entry; returned]
StatsHeader (16 bytes, repr(C)):
| Field | Type | Meaning |
|---|---|---|
version |
u32 | Layout version (currently 1) |
total |
u32 | Objects available in this snapshot |
returned |
u32 | Objects actually written |
entry_size |
u32 | sizeof(Entry) for forward-compatible parsing |
The syscall returns the number of bytes written. If returned < total the
buffer was too small; retry with a larger buffer. There is no silent truncation.
CpuStatsEntry (40 bytes):
| Field | Type | Source |
|---|---|---|
interrupt_count |
u64 | KPRCB.interrupt_count |
context_switch_count |
u64 | KPRCB.context_switch_count |
timer_tick_count |
u64 | KPRCB.timer_tick_count |
cpu_id |
u32 | KPRCB.cpu_id |
apic_id |
u32 | KPRCB.apic_id |
online |
u8 | 1 if the CPU is in the online set |
Semantics:
- One entry per online CPU (
cpu_local::cpu_count()). - Reads are lock-free per-CPU reads of
KPRCB_PAGES[cpu](the same mechanismis_pid_running_on_any_cpuuses). Each field is an aligned scalar (no torn word), but the set of fields is not one atomic instant: that CPU may update a counter between two of our reads. Treat it as a best-effort point-in-time sample, not a transactionally consistent snapshot. timer_tick_countcounts timer interrupts. It is not CPU busy time. For per-process/thread busy time useProcessSnapshotv2 (cpu_time);neotop v0.2computes CPU% from it. This class deliberately exposes no CPU%.interrupt_countis exposed for ABI completeness, but the kernel currently has no increment site for it, so it reads 0.timer_tick_countandcontext_switch_countare maintained.cpu_idis a scheduler CPU index;apic_ididentifies hardware. They are not assumed equal.- Online CPUs are currently
0..cpu_count(): NeoDOS brings APs online sequentially and does not support CPU hot-remove.
Fase 3 M3.2. Global work-stealing counters, read from the scheduler's atomics
(smp::STEAL_ATTEMPTS, smp::STEAL_SUCCESS). Fixed single struct: no header,
no array.
SmpStats (24 bytes, repr(C)):
| Field | Type | Source |
|---|---|---|
version |
u32 | Layout version (currently 1) |
_pad |
u32 | Alignment |
steal_attempts |
u64 | smp::STEAL_ATTEMPTS — every try_work_steal call |
steal_success |
u64 | smp::STEAL_SUCCESS — threads actually migrated |
The syscall returns 24 (the struct size); a smaller buffer is rejected with
-Inval (no partial write). neotop renders Work-steal: <success>/<attempts>
under the process/thread counts.
ThreadStatsEntry (24 bytes):
| Field | Type | Source |
|---|---|---|
tid |
u32 | Kthread.tid |
pid |
u32 | Kthread.pid |
cpu_id |
u32 | Kthread.cpu — CPU the thread is enqueued/running on; follows migration |
priority |
u8 | Kthread.priority (0 HIGH … 3 IDLE) |
state |
u8 | ThreadState::to_u8 (0 Ready, 1 Running, 2 Blocked, 3 Suspended, 4 Terminated) |
cpu_ticks |
u64 | Kthread.cpu_ticks — timer ticks charged to this thread |
Semantics:
- Enumerates all live KTHREADs, including kernel and per-CPU idle threads
(idle threads have
pid = 0). It does not use the Ob namespace, so it is not limited to user threads and needs no per-thread Ob object. - The global scheduler lock is held only while copying thread fields into a
staging buffer; user memory is written after the lock is released. No
Kthreadpointer ever reaches user space (no UAF window). - Best-effort snapshot: a thread created/terminated concurrently may or may not
appear, and
Kthread.cpucan change immediately after the copy (migration). At most one snapshot is in flight; a concurrent query returns-Againand may be retried. - A snapshot is capped at 128 entries;
total > returnedsignals truncation. cpu_ticksis a tick counter, not calibrated time and not a percentage. It increments once per timer tick while the thread is the running thread.
Phase 15-A. A single coherent process+thread inspection snapshot built from
Scheduler::snapshot_into (Phase 14-B). Unlike ThreadStats, processes and
threads come from one scheduler-consistent capture and names are included.
buffer = [ProcSnapshotHeader]
[ProcessInfoRaw; process_returned]
[ThreadInfoRaw; thread_returned]
ProcSnapshotHeader (32 bytes): version (2), process_total,
process_returned, thread_total, thread_returned, process_entry_size
(48), thread_entry_size (56), flags (bit0 = truncated).
ProcessInfoRaw (48 bytes): pid: u32, name: [u8; 32], thread_count: u32,
cpu_time: u64.
ThreadInfoRaw (56 bytes): tid: u32, pid: u32, name: [u8; 32],
state: u8 (0 Ready, 1 Running, 2 Blocked, 3 Suspended, 4 Terminated),
idle: u8, is_current: u8, _pad: u8, cpu: u32, cpu_time: u64.
Semantics:
- Sourced from the logical registries (
Scheduler.eprocesses/Scheduler.kthreads), not the run queues. A reaped object does not appear; aTerminated-but-not-yet-reaped thread does. - Copied under the global
SCHEDULERmutex; the lock is released before any user-memory write. Read-only, no kernel pointer escapes. cpufor a Running thread is theKPRCB.current_threadowner; otherwise the scheduler'sKthread.cpuassignment.is_currentmarks the authoritative owner (never inferred fromstate).idlemarks per-CPU idle threads (distinct namesidle/0..n).cpu_timeis the Phase 15-A.1 authoritative monotonic CPU execution counter (timer intervals): per-thread onThreadInfoRaw, summed over the process's threads onProcessInfoRaw(idle threads contribute 0). Two snapshots over a measured interval yield a real CPU percentage; the first sample has no delta. Seedocs/scheduler/scheduler.md.- Deterministic ordering: processes by
pid, threads bytid. - Truncation is explicit (
flagsbit0 and/or*_returned < *_total); there is no silent loss. At most one snapshot is in flight (-Againon contention). - Names are the bounded Phase 14-A
KernelName(ASCII,NAME_MAX = 32).
| Version | Change |
|---|---|
| 1 (Phase 15-A) | Initial process+thread snapshot (records 40/48 bytes) |
| 2 (Phase 15-A.1) | Append cpu_time: u64 to both records (48/56 bytes) |
v2 is not byte-compatible with v1 (record and header sizes changed), so
consumers validate version and *_entry_size before parsing; both sides
reject the other's layout rather than misparse it.
Class 3 (Process) now aggregates its threads' states under the scheduler lock
instead of thread_count == 0 ? Running : Ready:
| Value | Meaning |
|---|---|
| 0 | Ready — no Running thread, at least one Ready |
| 1 | Running — at least one thread Running on some CPU |
| 2 | Blocked — live threads exist, all Blocked/Suspended |
| 3 | Terminated — no live thread |
The struct layout is unchanged.
- Process names:
Eprocess.name(Phase 14-A) is the name source; useProcessSnapshot(class 26).ProcessArgsstill returns the calling process's args, not the queried process's. - CPU% / busy time:
ProcessSnapshotv2 exposescpu_time(Phase 15-A.1 monotonic execution accounting);neotop v0.2derives CPU% from two samples.CpuStats/ThreadStatsstill carry only tick counters (not busy time). - CPU hotplug:
onlineis derived from the contiguous online range. sys_ob_enumtruncation: directory enumeration still has no pagination. The new stats classes avoid silent loss viatotal/returned;sys_ob_enumitself is unchanged to preserve ABI v8.
Supports 45 set classes:
| Class | Name | Description |
|---|---|---|
| 0 | ProcessPriority | Set process priority |
| 1 | ThreadPriority | Set thread priority |
| 2 | ObjectName | Rename object |
| 3 | Security | Set DACL |
| 4 | ProcessTerminate | Kill process |
| 5 | KeyboardLayout | Set layout (1=en, 2=es, 3=de) |
| 6 | VfsRename | Rename VFS file/dir |
| 7 | WriteContent | Write to VFS file at handle offset |
| 8 | SetCwd | Change working directory |
| 9 | SetVolumeLabel | Set volume label |
| 10 | TimerStart | Start timer (oneshot/periodic) |
| 11 | TimerCancel | Cancel running timer |
| 12 | SemaphoreRelease | Increment semaphore count |
| 13 | SectionMapView | Map Section into process address space |
| 14 | SectionUnmapView | Unmap Section view |
| 15 | FileCreate | Create VFS file |
| 16 | FileDelete | Delete VFS file |
| 17 | SetProcessVt | Switch virtual terminal |
| 18 | SocketConnect | Connect TCP socket |
| 19 | SocketBind | Bind UDP/TCP socket |
| 20 | SocketListen | Listen on TCP socket |
| 21 | SocketSend | Send data on socket |
| 22 | SocketClose | Close socket |
| 23 | RegistryCreateKey | Create registry key |
| 24 | RegistryDeleteKey | Delete registry key |
| 25 | RegistrySetValue | Set registry value |
| 26 | RegistryDeleteValue | Delete registry value |
| 27 | SetNicIp | Set NIC IP address and subnet mask |
| 28 | SetNicGateway | Set NIC default gateway (0.0.0.0 = unset) |
| 29 | SocketBindNic | Pin a socket to a NIC for send-interface selection (u32 LE nic id) — #538 |
| 33 | ServiceStart | Start a service (Stopped/Failed → Starting → Running) |
| 34 | ServiceStop | Stop a running service (Running → Stopping → Stopped) |
| 35 | ServiceRestart | Restart a service (stop + start atomically) |
| 36 | ServiceSetConfig | Modify service configuration (start type, restart policy, max failures) |
| 37 | PowerShutdown | Initiate coordinated system shutdown |
| 38 | PowerReboot | Initiate coordinated system reboot |
| 39 | FsckRepair | Run fsck with repair flag (buf[0] != 0 = repair) |
| 43 | KeyboardSetLayout | Set layout by name (string) — \Device\Keyboard |
| 44 | KeyboardSetRepeatDelay | Set repeat delay in ms (u32 LE) — \Device\Keyboard |
| 45 | KeyboardSetRepeatRate | Set repeat rate in cps (u32 LE) — \Device\Keyboard |
| 46 | KeyboardSetLeds | Set LED state byte — \Device\Keyboard |
| 47 | KeyboardSetModifier | Set modifier byte (admin) — \Device\Keyboard |
| 49 | SetHostname | Set system hostname (REG_SZ) — any Ob object, admin only |
| 50 | DateTime | Set the system (RTC) clock from a SysDateTime (7 bytes) — \Global\Info\DateTime only, admin only |
| 51 | SetForegroundProcess | Mark the target Process as the caller VT's foreground process (Ctrl+C target) — used by NeoShell |
Enumerate a directory fd. Writes ObEnumEntry structs (52 bytes each) into the
user buffer. Returns entry count.
#[repr(C)]
pub struct ObEnumEntry {
pub id: ObId,
pub obj_type: u32,
pub name: [u8; 32],
pub mode: u16,
pub _pad: [u8; 2],
pub size: u32,
}Wait on up to N handles, with wait type (0=ANY, 1=ALL) and timeout in ms.
- Process: signaled when child exits (exit code in native_id)
- Pipe: signaled when data is available (pipe read end)
- Event: signaled on
set(manual-reset stays set until cleared) - Timer: signaled on expiry
- Thread: signaled on termination (join)
- Semaphore: signaled when count > 0
Non-blocking check: Pipe, Semaphore, and Timer perform an immediate check before entering the KWait block path. This prevents unnecessary context switches.
Delete an object from the namespace and object table. Calls on_destroy() if
the object has a custom ObOperations impl. Fails with -RefCountHeld if the
refcount indicates open handles or namespace children.
URN is a frontend of Ob — all URI schemes (file://, device://,
registry://, kobj://) resolve via ob_open_path() internally.
// URN resolving:
// "file://C:/foo.txt" -> ob_open_path("\\Global\\FileSystem\\C:\\foo.txt")
// "device://Tcp" -> ob_open_path("\\Device\\Tcp")
// "registry://..." -> ob_open_path("\\Registry\\...")UrnHandle wraps a kernel fd returned by ob_open_path(). There are 19
dedicated URN tests.
| Value | Name | Meaning |
|---|---|---|
| 0 | Success | Operation succeeded |
| -1 | NotFound | Object path not found |
| -2 | AlreadyExists | Object already exists at path |
| -3 | InvalidParam | Bad argument |
| -4 | RefCountHeld | Object has active references |
| -5 | OutOfMemory | Allocation failure |
| -6 | AccessDenied | Security check failed |
| -7 | NotSupported | Operation not supported for this type |
| -8 | InvalidType | Wrong ObType for operation |
| -9 | TableFull | Object table capacity exhausted |
The legacy kobj/ subsystem was eliminated in v0.46. All objects now use the
Ob architecture. The mapping is straightforward:
KObjType::X -> ObType::X
kobj_register(t,n,id) -> ob_create_object(t,n,id,0,None)
kobj_lookup(id) -> ob_lookup(id)
For the complete design history, evolution from KOBJ v1 through Ob unification,
namespace design rationale, and future roadmap, see:
docs/kernel/obj-arch.md — historical design companion document.
The following are locked decisions for the Ob architecture (archived from
obj-arch.md §15):
- ObObject is mandatory. Every kernel resource MUST be an
ObObjectin the global object table. No parallel resource tracking outside Ob. - ObId is unique and never reused.
next_idincrements monotonically. - ObType enum is the unique discriminator. No second enum, no type punning
via native_id. Adding a new resource type means adding one variant to
ObType. - All handles are ObHandles. HandleEntry stores only
object_id: ObIdandoffset: u64. The old per-type kind field is eliminated. - sys_close works via Ob. Closes the handle, calls
ob_release_object, triggerson_destroyif refcount reaches zero. - Ob namespace is hierarchical. Paths like
\Global\FileSystem\C:\.... No flat namespace. - Security is integrated into Ob.
SeAccessCheckis called onob_open_path. Every object has a DACL. - URN is a frontend of Ob. All URI schemes resolve via
ob_open_path(). - Migration is incremental. Legacy syscalls coexist with Ob syscalls during migration. No flag day.
- ObOperations trait is the extension point. Custom
on_destroyfor type-specific cleanup. The trait can grow new methods as needed.