Skip to content

Commit 32a18d2

Browse files
committed
Release v0.0.4
Builds on v0.0.3. New in this release: - A sturdier architecture. The bulk of this release is a deep structural pass that follows through on the sealed-core groundwork from v0.0.3. The scheduler and process engine — historically a handful of enormous files that the rest of the kernel reached into freely — is now split into clearly separated layers with a small, explicit set of entry points between them, and the largest source files are broken into focused modules. A single result-and-error vocabulary runs across every subsystem boundary. Build-time checks enforce the rules: scheduling and process state can only change through the engine's own entry points, and lower layers can't reach up into higher ones. None of this changes what programs see — it makes the kernel far easier to reason about and much harder to break one part without the build catching it, which is what lets new work land cleanly from here. - More faithful signals. Real-time signals now queue in order and respect the per-user limit on pending signals; a signal that interrupts an open-ended wait no longer wrongly restarts it; a memory fault arrives as a normal, catchable signal rather than an unconditional kill; and a debugger can reliably inject signals into a program it controls. Software that depends on precise signal behavior — shells, job control, language runtimes — behaves the way its authors expect. - Per-process CPU timers. Programs can arm timers that fire based on the processor time they actually consume, not just elapsed wall-clock time. - Real memory locking. Locking memory now genuinely pins it in RAM and faults it in immediately, and a program can ask that everything it maps from then on stays locked too. If the system runs out of memory while servicing a fault, it now stops just the offending program with a clean termination instead of putting the whole machine at risk. - Durable, precise file writes. Flushing a file writes its pending pages through to storage, and a range flush writes back exactly the bytes requested, so data lands when a program asks for it. Pipe buffer capacity can be queried and resized. - Correct directory-relative open permissions. Opening a file by a path relative to an open directory now runs the same permission check as opening it by full path, closing a gap where the relative form could skip the check. - Truer introspection. Resource-usage accounting reports real minor and major page-fault counts; the per-process status view reports the real open-file ceiling; a per-process login identity is tracked and inherited across programs. The processor listing shows one entry per online core, and the load average excludes idle sleepers, so standard monitoring tools read the values they expect. - Networking refinements. Non-blocking is honored per individual send and receive rather than only as a socket-wide mode, local socket pairs preserve message boundaries for datagram and packet types, and loopback delivery was corrected. - Better device and process fidelity. Terminal window size can be set and read back so full-screen terminal programs lay themselves out correctly; the disk now appears in the system's device tree so enumeration tools find it; creating a device node routes it to the right driver; and a child carries its parent's resource limits across a fork or clone. See README.md for what runs today, and docs/ for the architecture, the verification recipe, and the clean-room policy.
1 parent c15ca1d commit 32a18d2

310 files changed

Lines changed: 21992 additions & 14344 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/sbom.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ jobs:
100100
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
101101
run: |
102102
gh release upload "$REF_NAME" \
103-
cyphera-kernel-*-sbom.*.json \
103+
cyphera-kernel-*-sbom.cdx.json \
104+
cyphera-kernel-*-sbom.spdx.json \
104105
cyphera-kernel-*-sbom.*.json.sigstore.json \
105106
cyphera-kernel-*.provenance.intoto.jsonl \
106107
--clobber

Cargo.lock

Lines changed: 13 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frame/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
fn main() {
22
println!("cargo:rustc-check-cfg=cfg(coverage)");
3+
println!("cargo:rustc-check-cfg=cfg(cow_fork_forced_window)");
34
}

frame/src/arch/x86_64/fault.s

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Per-vector fault trampolines: snapshot the 15 GP registers gs-relative (order
2+
# mirrored by per_cpu::fault_gprs), then jmp — never call (a return address would
3+
# shift the CPU exception frame) — to the typed handler. GS is the per-CPU base in
4+
# both rings (no swapgs), so the stores are valid for kernel faults too.
5+
6+
.section .text
7+
8+
.macro SNAPSHOT_FAULT_GPRS
9+
mov %rax, %gs:0x20
10+
mov %rbx, %gs:0x28
11+
mov %rcx, %gs:0x30
12+
mov %rdx, %gs:0x38
13+
mov %rsi, %gs:0x40
14+
mov %rdi, %gs:0x48
15+
mov %rbp, %gs:0x50
16+
mov %r8, %gs:0x58
17+
mov %r9, %gs:0x60
18+
mov %r10, %gs:0x68
19+
mov %r11, %gs:0x70
20+
mov %r12, %gs:0x78
21+
mov %r13, %gs:0x80
22+
mov %r14, %gs:0x88
23+
mov %r15, %gs:0x90
24+
.endm
25+
26+
.global de_trampoline
27+
de_trampoline:
28+
SNAPSHOT_FAULT_GPRS
29+
jmp handle_divide_error
30+
31+
.global of_trampoline
32+
of_trampoline:
33+
SNAPSHOT_FAULT_GPRS
34+
jmp handle_overflow
35+
36+
.global br_trampoline
37+
br_trampoline:
38+
SNAPSHOT_FAULT_GPRS
39+
jmp handle_bound
40+
41+
.global ud_trampoline
42+
ud_trampoline:
43+
SNAPSHOT_FAULT_GPRS
44+
jmp handle_invalid_opcode
45+
46+
.global gp_trampoline
47+
gp_trampoline:
48+
SNAPSHOT_FAULT_GPRS
49+
jmp handle_gpf
50+
51+
.global pf_trampoline
52+
pf_trampoline:
53+
SNAPSHOT_FAULT_GPRS
54+
jmp handle_page_fault
55+
56+
.global mf_trampoline
57+
mf_trampoline:
58+
SNAPSHOT_FAULT_GPRS
59+
jmp handle_x87
60+
61+
.global ac_trampoline
62+
ac_trampoline:
63+
SNAPSHOT_FAULT_GPRS
64+
jmp handle_alignment
65+
66+
.global xm_trampoline
67+
xm_trampoline:
68+
SNAPSHOT_FAULT_GPRS
69+
jmp handle_simd

frame/src/arch/x86_64/idt.rs

Lines changed: 82 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,31 @@ use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame, Pag
33

44
use super::tss::DOUBLE_FAULT_IST_INDEX;
55

6+
core::arch::global_asm!(include_str!("fault.s"), options(att_syntax));
7+
8+
extern "C" {
9+
fn de_trampoline();
10+
fn of_trampoline();
11+
fn br_trampoline();
12+
fn ud_trampoline();
13+
fn gp_trampoline();
14+
fn pf_trampoline();
15+
fn mf_trampoline();
16+
fn ac_trampoline();
17+
fn xm_trampoline();
18+
}
19+
620
static IDT: Once<InterruptDescriptorTable> = Once::new();
721

822
pub fn init() {
923
let idt = IDT.call_once(|| {
1024
let mut idt = InterruptDescriptorTable::new();
1125

12-
idt.divide_error.set_handler_fn(handle_divide_error);
1326
idt.debug.set_handler_fn(handle_debug);
1427
idt.non_maskable_interrupt.set_handler_fn(handle_nmi);
1528
idt.breakpoint
1629
.set_handler_fn(handle_breakpoint)
1730
.set_privilege_level(x86_64::PrivilegeLevel::Ring3);
18-
idt.overflow.set_handler_fn(handle_overflow);
19-
idt.bound_range_exceeded.set_handler_fn(handle_bound);
20-
idt.invalid_opcode.set_handler_fn(handle_invalid_opcode);
2131
idt.device_not_available.set_handler_fn(handle_device_na);
2232

2333
// SAFETY: set_stack_index requires the IST index to name a
@@ -36,14 +46,36 @@ pub fn init() {
3646
idt.invalid_tss.set_handler_fn(handle_invalid_tss);
3747
idt.segment_not_present.set_handler_fn(handle_segment_np);
3848
idt.stack_segment_fault.set_handler_fn(handle_stack_seg);
39-
idt.general_protection_fault.set_handler_fn(handle_gpf);
40-
idt.page_fault.set_handler_fn(handle_page_fault);
41-
idt.x87_floating_point.set_handler_fn(handle_x87);
42-
idt.alignment_check.set_handler_fn(handle_alignment);
4349
idt.machine_check.set_handler_fn(handle_machine_check);
44-
idt.simd_floating_point.set_handler_fn(handle_simd);
4550
idt.virtualization.set_handler_fn(handle_virt);
4651

52+
// SAFETY: each synchronous user-fault vector is installed by address to
53+
// a register-snapshot trampoline (gs-relative stores + jmp to the typed
54+
// handler, leaving the CPU exception frame intact, so the handler runs
55+
// as a direct gate would). The symbols are crate-local `.text`, valid
56+
// for the program's lifetime; set_handler_addr only stores the gate's
57+
// target.
58+
unsafe {
59+
idt.divide_error
60+
.set_handler_addr(x86_64::VirtAddr::new(de_trampoline as *const () as u64));
61+
idt.overflow
62+
.set_handler_addr(x86_64::VirtAddr::new(of_trampoline as *const () as u64));
63+
idt.bound_range_exceeded
64+
.set_handler_addr(x86_64::VirtAddr::new(br_trampoline as *const () as u64));
65+
idt.invalid_opcode
66+
.set_handler_addr(x86_64::VirtAddr::new(ud_trampoline as *const () as u64));
67+
idt.general_protection_fault
68+
.set_handler_addr(x86_64::VirtAddr::new(gp_trampoline as *const () as u64));
69+
idt.page_fault
70+
.set_handler_addr(x86_64::VirtAddr::new(pf_trampoline as *const () as u64));
71+
idt.x87_floating_point
72+
.set_handler_addr(x86_64::VirtAddr::new(mf_trampoline as *const () as u64));
73+
idt.alignment_check
74+
.set_handler_addr(x86_64::VirtAddr::new(ac_trampoline as *const () as u64));
75+
idt.simd_floating_point
76+
.set_handler_addr(x86_64::VirtAddr::new(xm_trampoline as *const () as u64));
77+
}
78+
4779
idt[crate::intr::lapic::TIMER_VECTOR].set_handler_fn(handle_timer);
4880
idt[crate::intr::lapic::RESCHED_IPI_VECTOR].set_handler_fn(handle_resched_ipi);
4981
idt[crate::intr::lapic::TLB_SHOOTDOWN_VECTOR].set_handler_fn(handle_tlb_shootdown);
@@ -54,7 +86,11 @@ pub fn init() {
5486
idt.load();
5587
}
5688

89+
#[no_mangle]
5790
extern "x86-interrupt" fn handle_divide_error(frame: InterruptStackFrame) {
91+
if from_user(&frame) {
92+
try_deliver_user_fault(&frame, 0, 0, 0);
93+
}
5894
panic_with_frame("#DE divide-by-zero", &frame, None);
5995
}
6096

@@ -112,23 +148,26 @@ extern "x86-interrupt" fn handle_breakpoint(mut frame: InterruptStackFrame) {
112148
crate::println!("#BP breakpoint @ {:#x}", frame.instruction_pointer.as_u64());
113149
}
114150

151+
#[no_mangle]
115152
extern "x86-interrupt" fn handle_overflow(frame: InterruptStackFrame) {
153+
if from_user(&frame) {
154+
try_deliver_user_fault(&frame, 4, 0, 0);
155+
}
116156
panic_with_frame("#OF overflow", &frame, None);
117157
}
118158

159+
#[no_mangle]
119160
extern "x86-interrupt" fn handle_bound(frame: InterruptStackFrame) {
161+
if from_user(&frame) {
162+
try_deliver_user_fault(&frame, 5, 0, 0);
163+
}
120164
panic_with_frame("#BR bound range exceeded", &frame, None);
121165
}
122166

167+
#[no_mangle]
123168
extern "x86-interrupt" fn handle_invalid_opcode(frame: InterruptStackFrame) {
124169
if from_user(&frame) {
125-
if let Some(h) = crate::user::user_fault_handler() {
126-
crate::println!(
127-
"#UD from user @ rip={:#x}; killing process",
128-
frame.instruction_pointer.as_u64()
129-
);
130-
h(0, 6, 0);
131-
}
170+
try_deliver_user_fault(&frame, 6, 0, 0);
132171
}
133172
panic_with_frame("#UD invalid opcode", &frame, None);
134173
}
@@ -153,19 +192,15 @@ extern "x86-interrupt" fn handle_stack_seg(frame: InterruptStackFrame, err: u64)
153192
panic_with_frame("#SS stack-segment fault", &frame, Some(err));
154193
}
155194

195+
#[no_mangle]
156196
extern "x86-interrupt" fn handle_gpf(frame: InterruptStackFrame, err: u64) {
157197
if from_user(&frame) {
158-
if let Some(h) = crate::user::user_fault_handler() {
159-
crate::println!(
160-
"#GP from user @ rip={:#x} err={err:#x}; killing process",
161-
frame.instruction_pointer.as_u64()
162-
);
163-
h(0, 13, err);
164-
}
198+
try_deliver_user_fault(&frame, 13, err, 0);
165199
}
166200
panic_with_frame("#GP general protection fault", &frame, Some(err));
167201
}
168202

203+
#[no_mangle]
169204
extern "x86-interrupt" fn handle_page_fault(
170205
mut frame: InterruptStackFrame,
171206
err: PageFaultErrorCode,
@@ -193,15 +228,7 @@ extern "x86-interrupt" fn handle_page_fault(
193228
return;
194229
}
195230
}
196-
if let Some(h) = crate::user::user_fault_handler() {
197-
crate::println!(
198-
"#PF from user @ rip={:#x} cr2={:#x} err={:?}; killing process",
199-
frame.instruction_pointer.as_u64(),
200-
cr2,
201-
err
202-
);
203-
h(cr2, 14, err.bits());
204-
}
231+
try_deliver_user_fault(&frame, 14, err.bits(), cr2);
205232
}
206233
crate::println!(
207234
"#PF page fault @ rip={:#x} cr2={:#x} err={:?}",
@@ -216,19 +243,42 @@ fn from_user(frame: &InterruptStackFrame) -> bool {
216243
(frame.code_segment.0 & 3) == 3
217244
}
218245

246+
fn try_deliver_user_fault(frame: &InterruptStackFrame, vector: u8, error: u64, addr: u64) {
247+
if let Some(h) = crate::user::user_fault_signal() {
248+
let mut tf = crate::user::fault_trapframe(
249+
frame.instruction_pointer.as_u64(),
250+
frame.cpu_flags.bits(),
251+
frame.stack_pointer.as_u64(),
252+
);
253+
h(&mut tf, vector, error, addr);
254+
}
255+
}
256+
257+
#[no_mangle]
219258
extern "x86-interrupt" fn handle_x87(frame: InterruptStackFrame) {
259+
if from_user(&frame) {
260+
try_deliver_user_fault(&frame, 16, 0, 0);
261+
}
220262
panic_with_frame("#MF x87 FPU error", &frame, None);
221263
}
222264

265+
#[no_mangle]
223266
extern "x86-interrupt" fn handle_alignment(frame: InterruptStackFrame, err: u64) {
267+
if from_user(&frame) {
268+
try_deliver_user_fault(&frame, 17, err, 0);
269+
}
224270
panic_with_frame("#AC alignment check", &frame, Some(err));
225271
}
226272

227273
extern "x86-interrupt" fn handle_machine_check(frame: InterruptStackFrame) -> ! {
228274
panic_with_frame("#MC machine check", &frame, None);
229275
}
230276

277+
#[no_mangle]
231278
extern "x86-interrupt" fn handle_simd(frame: InterruptStackFrame) {
279+
if from_user(&frame) {
280+
try_deliver_user_fault(&frame, 19, 0, 0);
281+
}
232282
panic_with_frame("#XM SIMD floating-point", &frame, None);
233283
}
234284

frame/src/arch/x86_64/syscall.s

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ syscall_entry:
44
mov %rsp, %gs:0x8
55
mov %gs:0x0, %rsp
66

7+
# TrapFrame trailing rcx/r11 slots: SYSCALL overwrites the user rcx/r11
8+
# (return rip/rflags), so there are no user values to record here; push
9+
# zeroes to size the stack frame to the full TrapFrame. Discarded by the
10+
# %gs:0x8 rsp reload on the return path.
11+
pushq $0
12+
pushq $0
713
push %rax
814
push %r15
915
push %r14

0 commit comments

Comments
 (0)