diff --git a/apps/rust/hello/Cargo.toml b/apps/rust/hello/Cargo.toml
new file mode 100644
index 0000000..cfaa5ee
--- /dev/null
+++ b/apps/rust/hello/Cargo.toml
@@ -0,0 +1,38 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+[package]
+name = "hello"
+version = "0.1.0"
+edition = "2021"
+build = "build.rs"
+
+[build-dependencies]
+# build.rs depends on SEL4_OUT_DIR = "${ROOTDIR}/out/kata/kernel"
+sel4-config = { path = "../../system/components/kata-os-common/src/sel4-config" }
+
+[features]
+default = []
+# Used by sel4-config to extract kernel config
+CONFIG_PRINTING = []
+
+[lib]
+name = "hello"
+path = "hello.rs"
+crate-type = ["staticlib"]
+
+[dependencies]
+cstr_core = { version = "0.2.3", default-features = false }
+kata-os-common = { path = "../../system/components/kata-os-common", default-features = false }
+log = "0.4"
diff --git a/apps/rust/hello/Makefile b/apps/rust/hello/Makefile
new file mode 100644
index 0000000..8ce6e77
--- /dev/null
+++ b/apps/rust/hello/Makefile
@@ -0,0 +1,24 @@
+OUT_KATA    ?= $(OUT)/kata/riscv32-unknown-elf/release
+OUT_HELLO   ?= $(OUT_KATA)/apps/hello
+CARGO_OPTS  ?= --release
+
+# hello app staticlib (location set w/ --out-dir below)
+LIB_HELLO   := ${OUT_HELLO}/libhello.a
+# To satisfy Rust core deps with debug build
+LIB_LIBC    := ${OUT_KATA}/musllibc/build-temp/stage/lib/libc.a
+
+LD_FLAGS := -march=rv32imac -mabi=ilp32 -static -nostdlib -ftls-model=local-exec
+#DBG:=-g
+
+# NB: let cargo handle incremental build steps
+${OUT_HELLO}/hello.elf: hello.rs | tmp_check
+	SEL4_OUT_DIR=${OUT_KATA}/kernel kcargo build ${CARGO_OPTS} --target-dir ${OUT_HELLO} --out-dir ${OUT_HELLO}
+	riscv32-unknown-elf-gcc ${LD_FLAGS} $(DBG) ${LIB_HELLO} ${LIB_LIBC} -o ${OUT_HELLO}/hello.elf
+
+tmp_check:
+	mkdir -p $(OUT_HELLO)
+
+clean:
+	rm -rf ${OUT_HELLO}
+
+PHONY: tmp_check
diff --git a/apps/rust/hello/build.rs b/apps/rust/hello/build.rs
new file mode 100644
index 0000000..69753b1
--- /dev/null
+++ b/apps/rust/hello/build.rs
@@ -0,0 +1,34 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+extern crate sel4_config;
+use std::env;
+
+fn main() {
+    // If SEL4_OUT_DIR is not set we expect the kernel build at a fixed
+    // location relative to the ROOTDIR env variable.
+    println!("SEL4_OUT_DIR {:?}", env::var("SEL4_OUT_DIR"));
+    let sel4_out_dir = env::var("SEL4_OUT_DIR")
+        .unwrap_or_else(|_| format!("{}/out/kata/kernel", env::var("ROOTDIR").unwrap()));
+    println!("sel4_out_dir {}", sel4_out_dir);
+
+    // Dredge seL4 kernel config for settings we need as features to generate
+    // correct code: e.g. CONFIG_KERNEL_MCS enables MCS support which changes
+    // the system call numbering.
+    let features = sel4_config::get_sel4_features(&sel4_out_dir);
+    println!("features={:?}", features);
+    for feature in features {
+        println!("cargo:rustc-cfg=feature=\"{}\"", feature);
+    }
+}
diff --git a/apps/rust/hello/hello.rs b/apps/rust/hello/hello.rs
new file mode 100644
index 0000000..f168a26
--- /dev/null
+++ b/apps/rust/hello/hello.rs
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2021, Google LLC
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#![no_std]
+#![no_main]
+#![feature(asm)]
+#![feature(thread_local)]
+
+// TODO(sleffler): plumb logger to SDKRuntime to eliminate seL4_DebugPutChar
+//   (or provide a logger alternative)
+
+use kata_os_common::logger::KataLogger;
+use kata_os_common::sel4_sys;
+
+use sel4_sys::seL4_IPCBuffer;
+
+const PAGE_SIZE: usize = 4096;
+
+#[no_mangle]
+#[thread_local]
+static mut __sel4_ipc_buffer: *mut seL4_IPCBuffer = 0 as _;
+
+#[repr(align(4096))]
+#[allow(dead_code)]
+struct PageAlign {
+    data: [u8; PAGE_SIZE],
+}
+static mut STATIC_TLS: PageAlign = PageAlign {
+    data: [0u8; PAGE_SIZE],
+};
+
+#[no_mangle]
+pub fn _start() {
+    unsafe {
+        asm!("
+        .option push
+        .option norelax
+        la gp, __global_pointer$
+        la tp, {tls}
+        lui t1,0
+        add t1,t1,tp
+        sw a0,0(t1) # __sel4_ipc_buffer>
+        addi sp,sp,-16
+        sw a0, 12(sp)
+        sw a1, 8(sp)
+        sw a2, 4(sp)
+        sw a3, 0(sp)
+        .option pop
+        j main",
+            tls = sym STATIC_TLS,
+            options(noreturn),
+        )
+    };
+}
+
+// Message output is sent through the kata-os-logger which calls logger_log
+// to deliver data to the console. We use seL4_DebugPutChar to write to the
+// console which only works if DEBUG_PRINTING is enabled in the kernel.
+#[no_mangle]
+#[allow(unused_variables)]
+pub fn logger_log(_level: u8, msg: *const cstr_core::c_char) {
+    #[cfg(feature = "CONFIG_PRINTING")]
+    unsafe {
+        for c in cstr_core::CStr::from_ptr(msg).to_bytes() {
+            let _ = sel4_sys::seL4_DebugPutChar(*c);
+        }
+        let _ = sel4_sys::seL4_DebugPutChar(b'\n');
+    }
+}
+
+#[no_mangle]
+// XXX need SDK specification of main, use hack for now
+pub fn main(a0: u32, a1: u32, a2: u32, a3: u32) {
+    // Setup logger; (XXX belongs in the SDKRuntime)
+    static KATA_LOGGER: KataLogger = KataLogger;
+    log::set_logger(&KATA_LOGGER).unwrap();
+    log::set_max_level(log::LevelFilter::Trace);
+
+    // XXX maybe setup a heap (XXX belongs in the SDKRuntime)
+
+    log::info!("I am a Rust app, hear me roar!");
+    log::info!("a0 {:x} a1 {:x} a2 {:x} a3 {:x}", a0, a1, a2, a3);
+    log::info!("__sel4_ipc_buffer {:p}", unsafe { __sel4_ipc_buffer });
+    log::info!("Done, wimper ...");
+}
diff --git a/apps/system/components/ProcessManager/kata-proc-manager/src/sel4bundle/mod.rs b/apps/system/components/ProcessManager/kata-proc-manager/src/sel4bundle/mod.rs
index c3370c5..6d2361e 100644
--- a/apps/system/components/ProcessManager/kata-proc-manager/src/sel4bundle/mod.rs
+++ b/apps/system/components/ProcessManager/kata-proc-manager/src/sel4bundle/mod.rs
@@ -246,6 +246,8 @@ impl seL4BundleImpl {
         //
         // NB: beware the order of this must match *_SLOT above
         // TODO(sleffler): maybe construct the vec to avoid mismatches
+        // TODO(sleffler): the toplevel CNode has a fixed size which
+        //   can overflow when nframes is non-trivial
         let dynamic_objs = kata_object_alloc_in_toplevel(vec![
             // control/main-thread TCB
             ObjDesc::new(seL4_TCBObject, 1, TCB_SLOT),