Initialize the packaged GStreamer runtime in the voice host (#42631)

## Why

The voice helper previously supported only handshake and shutdown, without
validating that its packaged native runtime could be initialized safely.

## What changed

- Add an `initializeRuntime` protocol exchange that loads GStreamer and the
  required plugins from physical package paths without opening audio devices.
- Restrict plugin discovery, registry access, and native library search paths,
  and keep loaded libraries alive until the helper exits.
- Give initialization a dedicated deadline and terminate the owned helper when
  initialization is cancelled or fails.
- Allow binary-only Rust targets to disable the default Bazel library target.

## Testing

- Cover helper-only packages, cancellation, and environment filtering.
- Add an ignored integration test for initialization from a relocated prepared
  runtime and rejection of duplicate initialization.

GitOrigin-RevId: 8fff68fe26e52cb2e0722dc98fb5f124eddb0d4d
This commit is contained in:
Benjamin Carlsson
2026-09-03 22:41:26 +00:00
committed by copyberry
parent 280ae8b9fc
commit d979df154c
13 changed files with 386 additions and 16 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -4908,6 +4908,7 @@ dependencies = [
"codex-process-hardening",
"codex-realtime-webrtc",
"codex-utils-cargo-bin",
"libloading",
"pretty_assertions",
"tempfile",
"tokio",

View File

@@ -17,6 +17,7 @@ use crate::encode_frame;
use crate::message_reader::MessageReader;
const DEADLINE: Duration = Duration::from_secs(/*secs*/ 5);
const RUNTIME_INITIALIZATION_DEADLINE: Duration = Duration::from_secs(/*secs*/ 30);
/// Owns one helper. Dropping it terminates the process and leaves its waiter to reap it.
/// A successful handshake establishes compatibility only, not an active audio session.
@@ -27,6 +28,17 @@ pub struct VoiceHost {
}
impl VoiceHost {
/// Initialize the packaged native runtime without opening devices or starting a session.
pub async fn initialize_runtime(mut self) -> Result<Self> {
self.exchange(
Message::InitializeRuntime {},
Message::RuntimeReady {},
RUNTIME_INITIALIZATION_DEADLINE,
)
.await?;
Ok(self)
}
pub async fn connect(package: &CodexPackageLayout, build_commit: &str) -> Result<Self> {
let root = package.package_dir.as_path().canonicalize()?;
let name = if cfg!(windows) {
@@ -66,13 +78,16 @@ impl VoiceHost {
build_commit: build_commit.to_owned(),
},
Message::Ready {},
DEADLINE,
)
.await?;
Ok(host)
}
pub async fn close(mut self) -> Result<()> {
let result = self.exchange(Message::Close {}, Message::Closed {}).await;
let result = self
.exchange(Message::Close {}, Message::Closed {}, DEADLINE)
.await;
if result.is_err() {
self.process.terminate();
}
@@ -82,8 +97,13 @@ impl VoiceHost {
Ok(())
}
async fn exchange(&mut self, request: Message, expected: Message) -> Result<()> {
timeout(DEADLINE, async {
async fn exchange(
&mut self,
request: Message,
expected: Message,
deadline: Duration,
) -> Result<()> {
timeout(deadline, async {
self.process
.writer_sender()
.send(encode_frame(&request)?)
@@ -128,6 +148,11 @@ fn child_environment(vars: impl Iterator<Item = (OsString, OsString)>) -> HashMa
.then(|| Some((key, value.into_string().ok()?)))
.flatten()
})
.chain(
crate::RUNTIME_ENVIRONMENT
.into_iter()
.map(|(key, value)| (key.to_owned(), value.to_owned())),
)
.collect()
}

View File

@@ -14,6 +14,8 @@ fn forwards_only_explicit_device_network_and_os_inputs() {
("DYLD_INSERT_LIBRARIES", "loader"),
("PATH", "project"),
("GST_PLUGIN_PATH", "plugins"),
("GST_REGISTRY", "untrusted-registry"),
("GST_REGISTRY_FORK", "yes"),
("OPENAI_API_KEY", "secret"),
];
assert_eq!(
@@ -25,6 +27,37 @@ fn forwards_only_explicit_device_network_and_os_inputs() {
input[..3]
.iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.chain(crate::RUNTIME_ENVIRONMENT.map(|(key, value)| (key.into(), value.into())))
.collect()
);
}
#[cfg(unix)]
#[tokio::test]
async fn cancelling_initialization_terminates_the_owned_helper() -> anyhow::Result<()> {
let spawned = codex_utils_pty::spawn_pipe_process(
std::path::Path::new("/bin/sleep"),
&["30".to_owned()],
std::path::Path::new("/"),
&child_environment(std::iter::empty()),
/*arg0*/ &None,
&[],
)
.await?;
drop(spawned.stderr_rx);
let (_, unused_exit) = tokio::sync::oneshot::channel();
let host = super::VoiceHost {
process: spawned.session,
output: crate::message_reader::MessageReader::new(spawned.stdout_rx),
exit: unused_exit,
};
let mut initialization = Box::pin(host.initialize_runtime());
std::future::poll_fn(|context| {
assert!(std::future::Future::poll(initialization.as_mut(), context).is_pending());
std::task::Poll::Ready(())
})
.await;
drop(initialization);
tokio::time::timeout(std::time::Duration::from_secs(/*secs*/ 1), spawned.exit_rx).await??;
Ok(())
}

View File

@@ -5,6 +5,7 @@ mod protocol;
pub use client::VoiceHost;
pub use protocol::MAX_FRAME_BYTES;
pub use protocol::Message;
pub use protocol::RUNTIME_ENVIRONMENT;
pub use protocol::decode_frame;
pub use protocol::encode_frame;
pub use protocol::read_message;

View File

@@ -8,6 +8,20 @@ use serde::Serialize;
pub const MAX_FRAME_BYTES: usize = 256;
/// Fixed child settings prevent native initialization from scanning system plugins or caches.
pub const RUNTIME_ENVIRONMENT: [(&str, &str); 7] = [
("GST_PLUGIN_PATH", ""),
("GST_PLUGIN_PATH_1_0", ""),
("GST_PLUGIN_SYSTEM_PATH", ""),
("GST_PLUGIN_SYSTEM_PATH_1_0", ""),
(
"GST_REGISTRY",
if cfg!(windows) { "NUL" } else { "/dev/null" },
),
("GST_REGISTRY_UPDATE", "no"),
("GST_REGISTRY_FORK", "no"),
];
#[derive(Debug, Deserialize, PartialEq, Serialize)]
#[serde(
tag = "type",
@@ -18,6 +32,8 @@ pub const MAX_FRAME_BYTES: usize = 256;
pub enum Message {
Hello { protocol: u32, build_commit: String },
Ready {},
InitializeRuntime {},
RuntimeReady {},
Close {},
Closed {},
}

View File

@@ -3,4 +3,5 @@ load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "voice-host",
crate_name = "codex_voice_host",
crate_srcs = [],
)

View File

@@ -14,6 +14,7 @@ workspace = true
[dependencies]
codex-process-hardening = { workspace = true }
codex-realtime-webrtc = { workspace = true }
libloading = "=0.8.9"
[dev-dependencies]
anyhow = { workspace = true }

View File

@@ -1,12 +1,17 @@
# Private voice helper foundation
`codex-voice-host` establishes the inherited-pipe lifecycle for the proposed
bundled voice process. It does not open devices, load native plugins, negotiate
bundled voice process. It does not open devices, negotiate
WebRTC, or enable voice in the TUI. The existing CLI is unchanged.
Frames are a big-endian u32 length followed by at most 256 bytes of JSON. The
parent sends `hello` with protocol `1` and the helper's exact `buildCommit` before
receiving `ready`. It then sends `close` and receives `closed` before process exit.
After `ready`, the parent may send `initializeRuntime` once. `runtimeReady` means
the physical package's GStreamer library and seven explicit plugins initialized;
it does not mean an audio session started. Missing or invalid runtime files cause
the helper to exit without a readiness response or raw native diagnostics. The
client terminates the helper if initialization fails or exceeds its deadline.
Unknown fields, incompatible builds, invalid order and oversized frames fail
closed without echoing input. EOF exits even when the main worker cannot progress.
@@ -35,5 +40,20 @@ Omitting `--runtime` retains helper-only assembly.
This accepts a development runtime receipt, not an authenticated release. It
does not repeat native loader inspection or establish trust in the build inputs.
The helper still does not load these files; native loading, media/privacy controls,
linking against the prepared SDK and actual audio proof remain integration stages.
The helper opens only physical packaged paths. The parent fixes GStreamer search
paths to empty, disables registry updates/forking, and points its registry to the
OS null device. Windows loads use only the DLL's directory and System32. Native
libraries remain loaded until helper exit, even after partial initialization,
because GStreamer registers process-global callbacks. The small private C ABI
bootstrap does not expose native pointers to the parent or link native libraries
into ordinary Codex. The existing `libloading` dependency supplies OS loading.
Media/privacy controls, a full media binding layer and actual audio proof remain
integration stages; helper-only packages continue to support lifecycle calls.
The ignored `packaged_runtime` integration test uses real libraries prepared for
the host platform. From `codex-rs`, run
`CODEX_TEST_VOICE_RUNTIME=/absolute/prepared/runtime just test -p codex-voice-host --test packaged_runtime --run-ignored all`.
It copies and relocates the runtime with the real helper, checks client
initialization and close, and rejects duplicate initialization. This requires
native inputs separately; ordinary CI does not run this ignored test. It tests
helper loading, not microphone, speaker, backend, or release behavior.

View File

@@ -1,4 +1,6 @@
//! Private helper lifecycle foundation. No devices, native plugins, or media are opened yet.
//! Same-build helper lifecycle and opt-in private runtime initialization. No devices or media yet.
mod runtime;
use std::io;
use std::io::Write;
@@ -64,12 +66,29 @@ fn run() -> io::Result<()> {
let mut output = io::stdout().lock();
output.write_all(&encode_frame(&Message::Ready {})?)?;
output.flush()?;
match receiver.recv() {
Ok(Message::Close {}) => {
output.write_all(&encode_frame(&Message::Closed {})?)?;
output.flush()
}
Err(_) => Ok(()),
Ok(_) => Err(io::Error::other("invalid voice control sequence")),
let mut runtime = None;
loop {
let reply = match receiver.recv() {
Ok(Message::InitializeRuntime {}) => {
if runtime.is_some() {
return Err(io::Error::other("runtime already initialized"));
}
runtime = Some(runtime::Runtime::initialize()?);
Message::RuntimeReady {}
}
Ok(Message::Close {}) => {
output.write_all(&encode_frame(&Message::Closed {})?)?;
return output.flush();
}
Err(_) => return Ok(()),
Ok(
Message::Hello { .. }
| Message::Ready {}
| Message::RuntimeReady {}
| Message::Closed {},
) => return Err(io::Error::other("invalid voice control sequence")),
};
output.write_all(&encode_frame(&reply)?)?;
output.flush()?;
}
}

View File

@@ -0,0 +1,161 @@
//! Initialize only the physical package's native runtime on the helper worker.
//!
//! GStreamer registers process-global callbacks. Keep every opened library loaded
//! through process exit, including partial initialization failures. This private
//! bootstrap is not a media binding API and never opens an audio device.
use std::ffi::CString;
use std::ffi::c_char;
use std::ffi::c_void;
use std::io;
use std::mem::ManuallyDrop;
use std::path::Path;
use std::path::PathBuf;
use std::ptr;
use codex_realtime_webrtc::RUNTIME_ENVIRONMENT;
use libloading::Library;
type Init = unsafe extern "C" fn(*mut i32, *mut *mut *mut c_char, *mut *mut c_void) -> i32;
type LoadPlugin = unsafe extern "C" fn(*const c_char, *mut *mut c_void) -> *mut c_void;
type Unref = unsafe extern "C" fn(*mut c_void);
pub(super) struct Runtime {
_libraries: Vec<ManuallyDrop<Library>>,
}
impl Runtime {
pub(super) fn initialize() -> io::Result<Self> {
if RUNTIME_ENVIRONMENT.iter().any(|(key, value)| {
std::env::var_os(key).as_deref() != Some(std::ffi::OsStr::new(value))
}) {
return Err(io::Error::other("private runtime environment is required"));
}
let executable = std::env::current_exe()?.canonicalize()?;
let bin = executable.parent().ok_or_else(runtime_error)?;
let root = bin.parent().ok_or_else(runtime_error)?;
if bin.file_name().is_none_or(|name| name != "bin")
|| root.file_name().is_none_or(|name| name != "voice")
|| root
.parent()
.and_then(Path::file_name)
.is_none_or(|name| name != "codex-resources")
{
return Err(runtime_error());
}
let (core, directory, prefix, suffix) = if cfg!(target_os = "macos") {
(
"lib/libgstreamer-1.0.0.dylib",
"plugins",
"libgst",
".dylib",
)
} else if cfg!(all(target_os = "linux", target_env = "gnu")) {
(
"lib/libgstreamer-1.0.so.0",
"lib/gstreamer-1.0",
"libgst",
".so",
)
} else if cfg!(all(windows, target_env = "msvc")) {
("bin/gstreamer-1.0-0.dll", "bin", "gst", ".dll")
} else {
return Err(io::Error::other("unsupported native runtime platform"));
};
let core = private_file(root, core)?;
let plugins = [
"app",
"audioconvert",
"audioresample",
"coreelements",
"opus",
"rtp",
"rtpmanager",
]
.map(|name| private_file(root, &format!("{directory}/{prefix}{name}{suffix}")))
.into_iter()
.collect::<io::Result<Vec<_>>>()?;
let library = load(&core)?;
// SAFETY: These are stable GStreamer 1.x C signatures. The library stays
// loaded through process exit, and all calls occur on this one worker.
let (init, load_plugin, unref) = unsafe {
(
*library
.get::<Init>(b"gst_init_check\0")
.map_err(|_| runtime_error())?,
*library
.get::<LoadPlugin>(b"gst_plugin_load_file\0")
.map_err(|_| runtime_error())?,
*library
.get::<Unref>(b"gst_object_unref\0")
.map_err(|_| runtime_error())?,
)
};
let mut libraries = vec![library];
// SAFETY: GStreamer accepts null argc/argv and an omitted error output.
// Fixed child settings disable registry reads, scans, writes and forking.
if unsafe { init(ptr::null_mut(), ptr::null_mut(), ptr::null_mut()) } == 0 {
return Err(runtime_error());
}
for plugin in plugins {
// Preload with the restricted OS search policy before GStreamer
// opens the same module, including its private dependencies on Windows.
libraries.push(load(&plugin)?);
#[cfg(unix)]
let filename = {
use std::os::unix::ffi::OsStrExt;
CString::new(plugin.as_os_str().as_bytes())
};
#[cfg(windows)]
let filename = CString::new(plugin.to_str().ok_or_else(runtime_error)?);
let filename = filename.map_err(|_| runtime_error())?;
// SAFETY: The NUL-terminated filename lives through the call. A
// non-null result owns a plugin reference, released exactly once.
unsafe {
let plugin = load_plugin(filename.as_ptr(), ptr::null_mut());
if plugin.is_null() {
return Err(runtime_error());
}
unref(plugin);
}
}
Ok(Self {
_libraries: libraries,
})
}
}
fn private_file(root: &Path, relative: &str) -> io::Result<PathBuf> {
let path = root.join(relative);
if path.canonicalize()? != path || !path.is_file() {
return Err(runtime_error());
}
Ok(path)
}
fn load(path: &Path) -> io::Result<ManuallyDrop<Library>> {
// SAFETY: Callers supply only absolute, physical paths inside the installed
// trusted package. Native code runs only in this expendable helper process.
#[cfg(unix)]
let library = unsafe {
libloading::os::unix::Library::open(
Some(path),
libloading::os::unix::RTLD_NOW | libloading::os::unix::RTLD_LOCAL,
)
.map(Library::from)
};
#[cfg(windows)]
let library = unsafe {
libloading::os::windows::Library::load_with_flags(
path,
libloading::os::windows::LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
| libloading::os::windows::LOAD_LIBRARY_SEARCH_SYSTEM32,
)
.map(Library::from)
};
library.map(ManuallyDrop::new).map_err(|_| runtime_error())
}
fn runtime_error() -> io::Error {
io::Error::other("private audio runtime initialization failed")
}

View File

@@ -151,6 +151,9 @@ async fn installed_client_rejects_mixed_builds_and_missing_helper() -> Result<()
.close()
.await?;
assert!(VoiceHost::connect(&package, "wrong-build").await.is_err());
// Helper-only installations still handshake, but cannot claim native readiness.
let host = VoiceHost::connect(&package, &build_commit().await?).await?;
assert!(host.initialize_runtime().await.is_err());
// The same executable elsewhere in the package must not become a fallback.
fs::rename(&helper, bin.join(source.file_name().unwrap()))?;
assert!(

View File

@@ -0,0 +1,88 @@
//! Exercise native initialization with real prepared libraries in a relocated package.
use std::fs;
use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use anyhow::ensure;
use codex_install_context::InstallContext;
use codex_realtime_webrtc::VoiceHost;
use codex_utils_cargo_bin::cargo_bin;
use tokio::process::Command;
use tokio::time::timeout;
const DEADLINE: Duration = Duration::from_secs(/*secs*/ 10);
#[tokio::test]
#[ignore = "requires CODEX_TEST_VOICE_RUNTIME containing real prepared native libraries"]
async fn relocated_runtime_initializes_closes_and_rejects_duplicate_initialization() -> Result<()> {
let source = std::env::var_os("CODEX_TEST_VOICE_RUNTIME")
.context("set CODEX_TEST_VOICE_RUNTIME to a matching prepared runtime")?;
let source = fs::canonicalize(source)?;
ensure!(
source.join("runtime.json").is_file(),
"prepared runtime receipt missing"
);
let directory = tempfile::Builder::new()
.prefix("voice native package ")
.tempdir()?;
let root = directory.path().join("staging");
let runtime = root.join("codex-resources/voice");
fs::create_dir_all(&runtime)?;
let mut pending = vec![(source, runtime.clone())];
while let Some((source, destination)) = pending.pop() {
for entry in fs::read_dir(source)? {
let entry = entry?;
let target = destination.join(entry.file_name());
let kind = entry.file_type()?;
if kind.is_dir() {
fs::create_dir(&target)?;
pending.push((entry.path(), target));
} else {
ensure!(kind.is_file(), "runtime inputs must be physical files");
fs::copy(entry.path(), target)?;
}
}
}
let helper_source = cargo_bin("codex-voice-host")?;
let name = helper_source.file_name().context("helper filename")?;
fs::create_dir_all(runtime.join("bin"))?;
fs::copy(&helper_source, runtime.join("bin").join(name))?;
fs::create_dir(root.join("bin"))?;
let app_name = if cfg!(windows) { "codex.exe" } else { "codex" };
fs::write(root.join("bin").join(app_name), [])?;
fs::write(root.join("codex-package.json"), "{}")?;
let moved = directory.path().join("relocated package");
fs::rename(root, &moved)?;
let package = InstallContext::from_exe(
/*is_macos*/ cfg!(target_os = "macos"),
Some(&moved.join("bin").join(app_name)),
/*method_override*/ None,
)
.package_layout
.context("package layout")?;
let output = timeout(
DEADLINE,
Command::new(&helper_source)
.arg("--build-commit")
.kill_on_drop(true)
.output(),
)
.await??;
ensure!(output.status.success(), "helper build identity failed");
let build_commit = String::from_utf8(output.stdout)?.trim().to_owned();
VoiceHost::connect(&package, &build_commit)
.await?
.initialize_runtime()
.await?
.close()
.await?;
let host = VoiceHost::connect(&package, &build_commit)
.await?
.initialize_runtime()
.await?;
assert!(host.initialize_runtime().await.is_err());
Ok(())
}

View File

@@ -227,7 +227,8 @@ def codex_rust_crate(
Crates are only compiled in a single configuration across the workspace, i.e.
with all features in this list enabled. So use sparingly, and prefer to refactor
optional functionality to a separate crate.
crate_srcs: Optional explicit srcs; defaults to `src/**/*.rs`.
crate_srcs: Optional explicit library srcs; [] disables the library target.
Defaults to `src/**/*.rs` excluding binary entrypoints.
crate_edition: Rust edition override, if not default.
You probably don't want this, it's only here for a single caller.
proc_macro: Whether this crate builds a proc-macro library.
@@ -303,7 +304,7 @@ def codex_rust_crate(
binaries = DEP_DATA.get(native.package_name())["binaries"]
lib_srcs = crate_srcs or native.glob(["src/**/*.rs"], exclude = binaries.values(), allow_empty = True)
lib_srcs = crate_srcs if crate_srcs != None else native.glob(["src/**/*.rs"], exclude = binaries.values(), allow_empty = True)
maybe_deps = []