Skip to content
Closed
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
Binary file added .keys/install-cli_publisher_ed25519.pub
Binary file not shown.
Binary file added .keys/install-cli_publisher_mldsa65.pub
Binary file not shown.
Binary file added .keys/install_publisher_ed25519.pub
Binary file not shown.
Binary file added .keys/install_publisher_mldsa65.pub
Binary file not shown.
155 changes: 102 additions & 53 deletions abi/syscalls.toml

Large diffs are not rendered by default.

42 changes: 40 additions & 2 deletions src/arch/x86_64/asm/syscall.S
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ syscall_entry_asm:
below destroys them, so stash the user copies under the saved frame and
restore them on exit. One pad slot keeps 16-alignment since three
arg-saves are odd. Eleven slots -> rsp = 8 (mod 16). */
/* The callee-saved five, plus a pad to keep the parity the comment
above depends on. Nothing else on this path writes them to memory,
and Rust cannot read them either: by the time a handler runs, its
own prologue may already be using them. A forked child has to
resume with the parent's whole register state, so the capture has
to happen here or not at all. Six slots is 0 (mod 16), so every
offset below stays exactly where it was. */
push r15
push r14
push r13
push r12
push rbx
sub rsp, 8

sub rsp, 8
push rdx
push rsi
Expand All @@ -50,10 +64,23 @@ syscall_entry_asm:
mov r9, [rsp + 0x08]
mov r11, [rsp + 0x10]

/* Spill a6 as 7th arg, realigns to 16. */
/* a6 goes on the stack as the seventh argument; the eighth is a
pointer to the frame just saved. A pointer rather than a single
register because a forked child resumes with the parent's whole
state, and the frame holds all of it: the return address is the
saved rcx at offset 0x20 and the rest follows it. Taken before
the two pushes, so it points at the saved rax.

One push left rsp 16-aligned for the call. Two do not, so a pad goes
underneath them: the arguments themselves must sit at [rsp] and
[rsp+8] when the call executes. Cleanup grows from 0x10 to 0x20 for
the pad and the extra argument. */
mov rax, rsp
sub rsp, 8
push rax
push r11
call syscall_handler
add rsp, 0x10
add rsp, 0x20

/* SyscallSavedFrame{rax, r8, r9, r10, rcx, r11, rbp} at rsp. */
push rax
Expand All @@ -80,6 +107,17 @@ syscall_entry_asm:
pop rdx
add rsp, 8

/* The callee-saved five come back with their pad, in reverse. They
still hold the user's values, so this restores rather than
changes them; the point of saving was to give a supervisor a
complete frame to fork from. */
add rsp, 8
pop rbx
pop r12
pop r13
pop r14
pop r15

push rax
movabs rax, 0xffffffffffe08aff
and r11, rax
Expand Down
24 changes: 12 additions & 12 deletions src/arch/x86_64/syscall/manager/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use crate::security::hardening::speculation::kernel_entry;
use crate::syscall::contract::{dispatch as contract_dispatch, SyscallArgs};
use crate::syscall::numbers::SyscallNumber;
use crate::process::foreign::FRAME_WORDS;
use crate::syscall::types::errnos;

#[no_mangle]
Expand All @@ -28,28 +29,27 @@ pub(super) extern "C" fn syscall_handler(
arg4: u64,
arg5: u64,
arg6: u64,
frame: *const u64,
) -> u64 {
// A capsule reaching this point last controlled the branch predictors and
// the return stack. Refilling the RSB and re-asserting IBRS before any
// kernel branch runs is the whole point of the entry side, and it was the
// side with no caller: `kernel_exit` was wired on the return path, so
// mitigations were being applied leaving the kernel but not entering it.
// the return stack.
kernel_entry();

let Some(sc) = SyscallNumber::from_u64(number) else {
/*
* A number this kernel does not know. NONOS numbers are four
* character tags, so nothing legitimate lands here; a foreign
* binary's own numbering does. When the caller is a guest, its
* supervisor answers and the kernel stays ignorant of what was
* asked. Everyone else still gets ENOSYS.
*/
// A number this kernel does not know.
let args = [arg1, arg2, arg3, arg4, arg5, arg6];
return match crate::process::foreign::redirect(number, args, 0) {
// SAFETY: eK@nonos.systems - `frame` is the pointer the entry
// stub in syscall.S passed, naming the sixteen words it pushed
// on this kernel stack, which outlive this call. This is the
// one place that pointer is turned into a reference; everything
// downstream of it is safe code.
let saved = unsafe { &*(frame as *const [u64; FRAME_WORDS]) };
return match crate::process::foreign::redirect(number, args, saved) {
Some(value) => value,
None => (-(errnos::ENOSYS as i64)) as u64,
};
};
let _ = frame;
let result = contract_dispatch(sc, SyscallArgs::new([arg1, arg2, arg3, arg4, arg5, arg6]));
result.value as u64
}
67 changes: 67 additions & 0 deletions src/process/foreign/exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// NONOS Operating System
// Copyright (C) 2026 NONOS Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! `MkForeignExec`: the same guest, a different program.

use crate::syscall::microkernel::errnos::{ERRNO_INVAL, ERRNO_PERM};

use super::exec_context::fresh;
use super::peer_guard::in_user_half;

type Saved = Option<crate::arch::context::SavedUser>;

/// What a parked guest receives when its supervisor has replaced the program
/// under it.
pub(super) const EXECED: u64 = u64::MAX;

pub fn sys_foreign_exec(pid: u64, entry: u64, rsp: u64) -> i64 {
let Some(caller) = crate::process::current_pid() else {
return ERRNO_INVAL;
};
let pid = pid as u32;
if super::registry::supervisor_of(pid) != Some(caller) {
return ERRNO_PERM;
}
if rsp == 0 || !in_user_half(entry, 1) || !in_user_half(rsp, 1) {
return ERRNO_INVAL;
}
let Some(previous) = swap(pid, Some(fresh(entry, rsp))) else {
return ERRNO_INVAL;
};
drop_tls(pid);
// Answering is what releases the guest.
match super::trap_reply::answer_raw(pid, EXECED) {
0 => 0,
err => {
swap(pid, previous);
err
}
}
}

/// Put a context in place and hand back the one it displaced.
fn swap(pid: u32, ctx: Saved) -> Option<Saved> {
crate::process::with_process(pid, |p| {
core::mem::replace(&mut *p.saved_user_context.lock(), ctx)
})
}

/// Forget the thread pointer the replaced runtime set: the scheduler writes
/// the control block's base on every switch, so leaving it would put the new
/// image back on the old TLS the first time it is preempted.
fn drop_tls(pid: u32) {
crate::process::with_process(pid, |pcb| pcb.set_tls_base(0));
}
50 changes: 50 additions & 0 deletions src/process/foreign/exec_context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// NONOS Operating System
// Copyright (C) 2026 NONOS Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! The registers a program starts on.

use crate::arch::context::SavedUser;
use crate::process::userspace::{USER_CS, USER_DS, USER_RFLAGS};

/// Everything zero but the entry point, the stack and the constants the ABI
/// fixes.
pub(super) fn fresh(entry: u64, rsp: u64) -> SavedUser {
SavedUser {
rax: 0,
rbx: 0,
rcx: 0,
rdx: 0,
rsi: 0,
rdi: 0,
rbp: 0,
r8: 0,
r9: 0,
r10: 0,
r11: 0,
r12: 0,
r13: 0,
r14: 0,
r15: 0,
rip: entry,
rsp,
rflags: USER_RFLAGS,
cs: USER_CS as u64,
ss: USER_DS as u64,
// The thread pointer belongs to the runtime that is being replaced.
fs_base: 0,
gs_base: 0,
}
}
57 changes: 57 additions & 0 deletions src/process/foreign/exec_enter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// NONOS Operating System
// Copyright (C) 2026 NONOS Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Leaving the kernel on a program the thread was not running when it entered.

use crate::arch::context::SavedUser;
use crate::process::signal::SIGSEGV;
use crate::process::userspace::{restore_user_context_iretq, USER_CS, USER_DS};

const USER_VA_MAX: u64 = 0x0000_7FFF_FFFF_FFFF;

pub(super) fn enter(pid: u32) -> ! {
let ctx = crate::process::with_process(pid, |pcb| pcb.saved_user_context.lock().take());
match ctx.flatten() {
Some(c) if sane(&c) => resume(c),
// Nothing to run.
_ => crate::process::terminate_current_with_signal(SIGSEGV),
}
}

fn resume(ctx: SavedUser) -> ! {
/*
* The scheduler installs the control block's base on every switch, and
* this path deliberately does not go through the scheduler, so the
* register is written here as well.
*/
crate::arch::context::set_user_tls(ctx.fs_base);
/*
* SAFETY: eK@nonos.systems - `ctx` is a local, so it outlives the
* five pushes the restore makes below rsp. Its selectors are the
* user pair and rip/rsp are in the low half, checked above; the
* address space is this pid's own, which is already on cr3 because
* this thread is the one running.
*/
unsafe { restore_user_context_iretq(&ctx) }
}

fn sane(c: &SavedUser) -> bool {
c.cs == USER_CS as u64
&& c.ss == USER_DS as u64
&& c.rip <= USER_VA_MAX
&& c.rsp <= USER_VA_MAX
&& c.rsp != 0
}
52 changes: 52 additions & 0 deletions src/process/foreign/fork.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// NONOS Operating System
// Copyright (C) 2026 NONOS Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Duplicating a guest.

use crate::process::core::ProcessState;
use crate::syscall::microkernel::errnos::{ERRNO_INVAL, ERRNO_NOENT, ERRNO_PERM};

/// `MkForeignFork`: a second process holding the first one's register state,
/// with zero in its return register so the two can tell each other apart,
/// which is the whole of fork's contract to the program.
pub fn sys_foreign_fork(pid: u64) -> i64 {
let Some(caller) = crate::process::current_pid() else {
return ERRNO_INVAL;
};
let parent = pid as u32;
if super::registry::supervisor_of(parent) != Some(caller) {
return ERRNO_PERM;
}
let Some(state) = saved_state(parent) else {
// A guest that is not parked inside a syscall has no frame to copy.
return ERRNO_NOENT;
};
let child = match super::spawn::empty_guest(caller, b"fork") {
Ok(pid) => pid,
Err(e) => return e,
};
let mut frame = state;
frame.rax = 0;
crate::process::with_process(child, |pcb| {
*pcb.saved_user_context.lock() = Some(frame);
*pcb.state.lock() = ProcessState::New;
});
child as i64
}

fn saved_state(pid: u32) -> Option<crate::arch::context::SavedUser> {
crate::process::with_process(pid, |pcb| *pcb.saved_user_context.lock()).flatten()
}
38 changes: 38 additions & 0 deletions src/process/foreign/frame_cpu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// NONOS Operating System
// Copyright (C) 2026 NONOS Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! The one word of a guest's state that is not in the saved frame.

/// The user stack pointer.
#[inline]
pub fn user_rsp() -> u64 {
let rsp: u64;
/*
* SAFETY: eK@nonos.systems - reads `user_stack_saved` in PerCpuData
* at a compile-time offset. Kernel GS is still active on this path:
* neither sysret nor iretq has run, which is the same condition the
* sigreturn path relies on for the same read.
*/
unsafe {
core::arch::asm!(
"mov {0}, gs:[{off}]",
out(reg) rsp,
off = const crate::smp::percpu::layout::USER_STACK_SAVED,
options(nomem, nostack, preserves_flags),
);
}
rsp
}
Loading
Loading