Skip to content

host-native: Initial support for host-native tasks - #2736

Open
jamesmunns wants to merge 26 commits into
masterfrom
james/native-pt1
Open

jamesmunns wants to merge 26 commits into
masterfrom
james/native-pt1

Conversation

@jamesmunns

@jamesmunns jamesmunns commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

This PR builds on #2704 and #2705 by implementing the respective Arch traits on the native host architecture.

Closes #2706.

From a high level, this compiles the kernel and each task as binaries for the host architecture, and then launching the kernel process. The kernel process then spawns each task as a child OS process, and communicates with it using stdio. The child tasks emit system calls to stdout, and block for a response on stdin. The kernel then processes system calls, and performs scheduling using kern's actual scheduling logic, resuming one task at a time.

The kernel can be configured to operate at "wall clock" time (so delays/scheduling is real, and should appear roughly at the same speed hardware would), or non-realtime, meaning that the system will execute as fast as possible, fast-forwarding the clock whenever the system would sit in the idle task.

Host apps are configured the same way firmware apps are: we define a folder in app, and can add manifests in the TOML format. I've added host definitions for all of the necessary bits to make this work. You can now run the following command, and see the following output tracing raw syscalls that occur:

$ cargo xtask host-run --trace --stop-at 1000 app/host/demo.toml
    Finished `dev` profile [optimized + debuginfo] target(s) in 0.16s
     Running `target/debug/xtask host-run --trace --stop-at 1000 app/host/demo.toml`
building task-pong for the host
    Finished `dev` profile [optimized + debuginfo] target(s) in 0.10s
building task-ping for the host
    Finished `dev` profile [optimized + debuginfo] target(s) in 0.10s
building task-stub for the host
    Finished `dev` profile [optimized + debuginfo] target(s) in 0.09s
building host-demo for the host
    Finished `dev` profile [optimized + debuginfo] target(s) in 0.09s
wrote target/host/host-demo/host-config.ron
running target/host/debug/host-demo
[kernel t=0000000] task pong started: /Users/james/oxide/hubris/target/host/debug/task-pong (pid 21736)
[kernel t=0000000] task ping started: /Users/james/oxide/hubris/target/host/debug/task-ping (pid 21737)
[kernel t=0000000] task user_leds started: /Users/james/oxide/hubris/target/host/debug/task-stub (pid 21738)
[kernel t=0000000] task user_leds: RECV mask 0x0 from None
[kernel t=0000000] task      pong: REFRESH_TASK_ID
[kernel t=0000000] task      pong: resumed from REFRESH_TASK_ID with [02, 00, 00, 00, 00, 00]
[kernel t=0000000] task      pong: SET_TIMER Some(500) bits 0x1
[kernel t=0000000] task      pong: resumed from SET_TIMER with [00, 00, 00, 00, 00, 00]
[kernel t=0000000] task      pong: RECV mask 0x1 from None
[kernel t=0000000] task      ping: REFRESH_TASK_ID
[kernel t=0000000] task      ping: resumed from REFRESH_TASK_ID with [00, 00, 00, 00, 00, 00]
[kernel t=0000000] task      ping: SEND to pong: op 1 (5 bytes)
[kernel t=0000000] task      pong: resumed from RECV with [00, 01, 01, 05, 10, 00]
[kernel t=0000000] task      pong: REPLY to ping: code 0 (0 bytes)
[kernel t=0000000] task      pong: resumed from REPLY with [00, 00, 00, 00, 00, 00]
[kernel t=0000000] task      pong: RECV mask 0x1 from None
[kernel t=0000000] task      ping: resumed from SEND with [00, 00, 00, 00, 00, 00]
[kernel t=0000000] task      ping: process ended, MemoryAccess { address: None, source: User }
[kernel t=0000000] task      pong: resumed from RECV with [00, FFFF, 01, 00, 00, 00]
[kernel t=0000000] task      pong: SET_TIMER Some(1000) bits 0x1
[kernel t=0000000] task      pong: resumed from SET_TIMER with [00, 00, 00, 00, 00, 00]
[kernel t=0000000] task      pong: SEND to user_leds: op 3 (8 bytes)
[kernel t=0000000] task user_leds: resumed from RECV with [00, 00, 03, 08, 00, 00]
[kernel t=0000000] task user_leds: REPLY to pong: code 0 (0 bytes)
[kernel t=0000000] task user_leds: resumed from REPLY with [00, 00, 00, 00, 00, 00]
[kernel t=0000000] task user_leds: RECV mask 0x0 from None
[kernel t=0000000] task      pong: resumed from SEND with [00, 00, 00, 00, 00, 00]
[kernel t=0000000] task      pong: RECV mask 0x1 from None
[kernel t=0001000] task      idle: waited for the next deadline
[kernel t=0001000] task      pong: resumed from RECV with [00, FFFF, 01, 00, 00, 00]
[kernel t=0001000] task      pong: SET_TIMER Some(1500) bits 0x1
[kernel t=0001000] task      pong: resumed from SET_TIMER with [00, 00, 00, 00, 00, 00]
[kernel t=0001000] task      pong: SEND to user_leds: op 3 (8 bytes)
[kernel t=0001000] task user_leds: resumed from RECV with [00, 00, 03, 08, 00, 00]
[kernel t=0001000] task user_leds: REPLY to pong: code 0 (0 bytes)
[kernel t=0001000] task user_leds: resumed from REPLY with [00, 00, 00, 00, 00, 00]
[kernel t=0001000] task user_leds: RECV mask 0x0 from None
[kernel t=0001000] task      pong: resumed from SEND with [00, 00, 00, 00, 00, 00]
[kernel t=0001000] task      pong: RECV mask 0x1 from None
kernel: stopping: time reached the configured stop at 1000 ticks

This is staged as separate commits adding each major "chunk" of changes, though I opened this as a single PR as we sort of need all the pieces existing to get the "tada" experience of it all working.

For the oxide folks, see https://rfd.shared.oxide.computer/rfd/0753 for more contextual details.

This crate contains the protocol between a host-native `kern` impl
and a host-native `userlib` impl.
Also updates ping and pong tasks, and adds stub task.
@jamesmunns

Copy link
Copy Markdown
Contributor Author

(sorry for the one force-push to bring this up to date with the master branch, no more after this)

@jamesmunns

jamesmunns commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author
    Finished `dev` profile [optimized + debuginfo] target(s) in 1m 03s
     Running `target/debug/xtask dist app/host/demo.toml`
Error: Supervisor task (pong) is not at priority 0

Hmm, I will need to probably:

  • Make the dist command notice the target is host, and refuse to build and instead point you toward host-build/host-run.
  • Do something with the CI script to skip/do something different for host manifests.

@labbott labbott left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I support getting more feedback on the architecture AND I also support merging this in.

I think it generally looks good and we're likely to make more progress trying to get it to do things if it's in tree vs not. I'm putting my approve bits on because I am a CODEOWNER.

Comment thread sys/hostcall/src/lib.rs
Comment on lines +14 to +15
//! COBS encoding is used for framing, and messages are serialized using the
//! `postcard` crate.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remind me why we chose COBS? (No objections, I also didn't see it documented in RFD753)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add it there! COBS is my go-to for "turn a stream of bytes into a stream of frames". We use it for some other things as well (IPCC, iirc?). Since stdio is a "stream of bytes" medium, we want to turn that into discrete frames.

Comment on lines +19 to +22
//! Despite operating as multiple OS processes, the control flow between the
//! kernel and tasks is still lock-step (emulating single threaded behavior),
//! as all entities except for the currently running one will block waiting for
//! a reply from the currently running entity.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Paragraph of thinking out loud) It took me a minute to think about this but the key words here are control flow . The interaction between (blocking) sys calls and interrupts is the most useful part of this. Techncially we could have multiple tasks "running" if they aren't making syscalls but I think that should be fine because the interesting work should come from syscalls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I agree with your thinking out loud, but yeah, it is intentional that we do "lockstep control flow across processes" to be more faithful to the system we are emulating. It also is already how userlib + the kernel behave.

Comment thread build/util/src/lib.rs Resolved
Comment thread build/xtask/src/host.rs Resolved
Comment thread build/xtask/src/host.rs Resolved
Comment thread build/xtask/src/main.rs Outdated
verbose,
release,
cfg,
tasks,

@mkeeter mkeeter Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's confusing that an empty tasks builds nothing, rather than everything. Make it an Option and default to building everything if None? It's also helpful to have a -l / --list option, which prints available tasks to build.

Comment thread chips/host/chip.toml Resolved
Comment thread chips/host/memory.toml Resolved
Comment thread chips/host/memory.toml Resolved
Comment thread sys/abi/src/lib.rs Resolved
Comment thread sys/hostcall/tests/roundtrip.rs Outdated
.leases
.iter()
.map(|l| {
(l.attributes.0 & 2 != 0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the bitfield values here instead of 2?

Comment thread sys/hostcall/tests/roundtrip.rs Resolved
Comment thread sys/userlib/build.rs Resolved
Comment thread sys/kern/build.rs Resolved
Comment thread sys/kern/build.rs
base: 0usize,
size: 32usize,
attributes: RegionAttributes::empty(),
arch_data: crate::arch::host::compute_region_extension_data(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pre-existing condition, but it seems like we should just have a crate::arch::$xyz::compute_region_desc(base, size, attributes) -> RegionDesc, instead of repeating the arguments as member variables and in this function call.

@mkeeter mkeeter Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...or an even more abstracted approach: hide it inconst fn RegionDesc::new, which calls a constructor for the ArchImpl::RegionDescExt associated type? This require another helper trait, but removes all of the "manually selecting the correct arch module".

Comment thread sys/kern/build.rs Resolved
mkeeter added a commit that referenced this pull request Nov 4, 2026
While reviewing #2736 (which constructs a `TaskDesc` table in
`host_config`), I realized we shouldn't have to rely on human memory to
guarantee that regions are sorted.

@mkeeter mkeeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm generally feeling positive about the PR. I've got lots of little comments, and I'm mostly ambivalent about how many of them actually get changed before merging (versus after), but would like to hear your thoughts!

Comment thread sys/kern/src/arch/host/config.rs Outdated
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! # Configuration

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels architecturally iffy. The RON file is serving two purposes:

  • Defining the task table (task indices and executable paths). This is known at xtask time, and I don't think it should change for a given top-level host binary
  • Passing arguments to the executable (the tracing / realtime flags)

My intuition is to break this up into two pieces:

  • We can build the task table at compile time with codegen. I'd guess this would be in a build.rs script reading an environment variable populated by xtask – very similar to what we have here, but done at build time instead of runtime.
  • We can pass arguments by actually parsing std::env::args right here! They're simple enough that we probably don't need clap, although we could use it for nicer ergonomics (the Big Computer is fast enough).

If these changes are made, then the top-level binary would be executable directly from the command line (right now, it complains about a missing HUBRIS_HOST_CONFIG, which is helpful but a bit of a speed bump).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed, we now:

  • Bake in task slots to the tasks
  • Bake in a list of tasks to the kernel
  • Pass all the args through to the kernel using --

new invocation is:

cargo xtask host-run app/host/demo.toml -- --trace --stop-at 1000

Comment thread sys/kern/src/arch/host/mod.rs Resolved
Comment thread sys/kern/src/arch/host/mod.rs Outdated
// Safety: the pointer came from a `&mut Task` in the task table (see
// `set_current_task`), and this is only called between kernel entries,
// when no other reference into the table is live.
let index = unsafe { usize::from((*current).descriptor().index) };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: move the unsafe to have smaller scope, because I had trouble finding the unsafe part at first glance

Suggested change
let index = unsafe { usize::from((*current).descriptor().index) };
let index = usize::from(unsafe { &*current }.descriptor().index);

Comment thread sys/kern/src/arch/host/mod.rs Resolved
Comment thread sys/kern/src/arch/host/mod.rs Resolved
Comment thread sys/kern/src/arch/host/host_task.rs Resolved
Comment thread sys/kern/src/arch/host/host_task.rs Outdated
procs.resize_with(index + 1, || None);
}
if let Some(mut old) = procs[index].take() {
old.child.kill().ok();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably helpful to do some logging here.

Comment thread sys/kern/src/arch/host/host_task.rs Resolved
Comment thread sys/kern/src/arch/host/host_task.rs Resolved
Comment thread sys/kern/src/arch/host/host_task.rs Resolved
jamesmunns added a commit that referenced this pull request Nov 6, 2026
This is an extract of the task-slot binding mechanism I was building in
#2736.

With today's `const` (and `xtask`), we can actually skip the
post-linking patchup entirely, instead directly patching in via
environment variables, as `xtask` is aware of all renames/remappings
prior to building any tasks.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracking Issue: Running Tasks on Host/Native

3 participants