COOLJAPAN
← All posts

OxiSound 0.2.1 Released — A Zero-C PulseAudio Backend, Three OSC Decoder DoS Bugs Closed

OxiSound 0.2.1 ships oxisound-pulse — a brand-new, 100% Pure Rust PulseAudio/PipeWire native-protocol backend with zero C in its Linux audio path — plus three denial-of-service fixes in oxisound-osc's untrusted-UDP decode path (an out-of-bounds read, unbounded nesting, and a 32-bit integer wraparound), a new fuzz workspace, and a stream_stats() correctness fix. The sovereign audio device I/O layer for the COOLJAPAN ecosystem.

release oxisound pure-rust cooljapan noffi audio pulseaudio security fuzzing

An OSC receiver bound to a UDP socket has to decode whatever arrives on it — and until this release, a single crafted datagram (16384 open brackets, or roughly 2000 nested bundles, both well within the 65536-byte limit OscReceiver::recv already accepts) could send that decode into unbounded recursion and take the whole process down with a stack overflow.

Today we released OxiSound 0.2.1 — the COOLJAPAN Pure-Rust audio device I/O layer. This release closes three denial-of-service bugs in oxisound-osc’s untrusted-UDP-datagram decode path, and ships oxisound-pulse — a brand-new crate, its first-ever release — a 100% Pure Rust backend that speaks the PulseAudio native IPC protocol directly over a unix socket, giving Linux an audio path with genuinely zero C in it.

No libpulse. No alsa-lib in the new backend’s path — that’s the whole point of it existing. oxisound-pulse runs under #![forbid(unsafe_code)] and talks to the PulseAudio (or PipeWire, transparently, through its pipewire-pulse compatibility service) socket in ordinary safe Rust. The rest of the OxiSound facade is unchanged: it still compiles to a single static binary and runs anywhere Rust does, with the OS-boundary backends isolated and auto-selected by target.

Why OxiSound 0.2.1 is a game changer

A “Pure Rust” audio crate that hasn’t been pressure-tested on a few specific fronts still has real gaps:

OxiSound 0.2.1 ends all of that:

Technical Deep Dive: the Pulse wire path, three fixes in one file, and a new fuzz workspace

  1. oxisound-pulse’s architecture. A pulseaudio client owns one reactor thread per connection — the only thread that touches the socket. Stream handles exchange bytes with it over lock-free SPSC rings and never block on it directly. Playback is server-clocked: an empty ring parks a std::task::Waker on the reactor side instead of injecting silence, and the next write() wakes it — real back-pressure, no phantom latency on an idle stream. Four modules are host-independent and unit-tested everywhere, not just on Linux: env (socket/cookie discovery), format (PulseSampleFormat, f32 ⇄ wire-byte conversion), model (device mapping, buffer arithmetic), and timeout (block_on_timeout, the PULSE_*_TIMEOUT deadlines bounding every blocking control-plane round-trip). Only the thin protocol adapter itself is Linux-gated, via [target.'cfg(target_os = "linux")'.dependencies].
  2. All three OSC fixes live in crates/oxisound-osc/src/decode.rs, and share one root cause: untrusted-input arithmetic that assumed well-formed input. The alignment-padding bug assumed a prior string’s padding never overruns the buffer. The nesting bug assumed a well-behaved sender wouldn’t nest arbitrarily. The wraparound bug assumed pos + size fits in usize — true on 64-bit, false on 32-bit once size (a raw 4-byte length prefix under full sender control) gets close to u32::MAX. All three are now guarded explicitly rather than relying on downstream code to happen to fail safely.
  3. The fuzz workspace mirrors the oxitext/oxih5 pattern — its own top-level [workspace] table, not a member of the main one, so cargo +nightly fuzz build and cargo +nightly fuzz run <target> work standalone. osc_decode fuzzes exactly the untrusted-UDP-datagram path the three Security fixes above harden; smf_parse fuzzes oxisound_smf::parse against arbitrary .mid bytes.
  4. Workspace hygiene that quietly matters: the oxiaudio/oxiaudio-core bump from 0.2.0 to 0.2.1 pulled crossbeam-epoch 0.9.20 and an un-yanked spin 0.12.2 through the (dev) oxiaudio → oxifft → rayon chain, clearing an outstanding cargo deny check advisories failure. A new deny.toml exception lets jack-sys through specifically when jack (the safe binding, itself confined to the oxisound-jack quarantine crate) is its sole dependent — the ban had been firing on a dependency the architecture already quarantines correctly.

Getting Started

cargo add oxisound

Decode OSC safely — the same entry point now rejects a malicious datagram with a typed error instead of overrunning a buffer or overflowing the stack:

use oxisound_osc::{OscArg, OscMessage, OscPacket, encode, decode};

let packet = OscPacket::Message(OscMessage {
    address: "/synth/freq".to_string(),
    args: vec![OscArg::Float(440.0)],
});
let bytes = encode(&packet);
let decoded = decode(&bytes)?;
assert_eq!(decoded, packet);

// A well-formed address ("/a") whose type-tag string requests 16384
// levels of array nesting -- the exact attack payload from the audit --
// is now rejected during construction, within the first MAX_NESTING_DEPTH
// (32) loop iterations, instead of overflowing the stack on the first
// later traversal (Drop, Debug, PartialEq, or `encode`) of a 16384-deep tree.
let mut hostile: Vec<u8> = b"/a\0\0".to_vec();
let mut type_tags: Vec<u8> = vec![b','];
type_tags.extend(std::iter::repeat_n(b'[', 16384));
type_tags.extend(std::iter::repeat_n(b']', 16384));
type_tags.push(0);
while type_tags.len() % 4 != 0 {
    type_tags.push(0);
}
hostile.extend_from_slice(&type_tags);
assert!(decode(&hostile).is_err());

Open the new Pure-Rust PulseAudio/PipeWire backend — opt-in, so enable the feature first:

[dependencies]
oxisound = { version = "0.2.1", features = ["pulse"] }
// Enumerate PulseAudio/PipeWire endpoints (sinks -> is_output, sources -> is_input;
// a sink's monitor source is reported as an input named "<sink>.monitor").
let devices = oxisound::pulse_enumerate_devices()?;

// Playback on the server default, no libpulse, no alsa-lib.
let mut out = oxisound::pulse_output(oxisound::StreamConfig::stereo_48k())?;

Or drive oxisound-pulse directly, without the facade:

use oxisound_core::{AudioDevice, OutputStream, StreamConfig};
use oxisound_pulse::PulseDevice;

let device = PulseDevice::default_output()?;
let mut stream = device.open_output_concrete(StreamConfig::stereo_48k())?;
stream.write(&vec![0.0f32; 4_800 * 2])?; // 100 ms at 48 kHz stereo
stream.drain()?;

What’s New in 0.2.1

Tips

This is the foundation

OxiSound is the device I/O half of the COOLJAPAN audio story. Its sibling oxiaudio — which OxiSound itself depends on for the optional oxiaudio/type-bridge feature — handles audio processing, DSP, and codecs; OxiSound handles getting the samples to and from the hardware, now with a genuinely C-free path on Linux via oxisound-pulse. OxiSound is part of NoFFI, the COOLJAPAN initiative to replace every C/C++/Fortran/-sys FFI dependency in the Rust ecosystem with a clean, memory-safe, 100% Pure Rust implementation — this release moves the Linux audio backend itself one step closer to that line.

Repository: https://github.com/cool-japan/oxisound

Star the repo if you want your Linux audio path to have zero C in it and your UDP-facing decoder to reject hostile input instead of overflowing a stack.

The era of “Pure Rust audio, except for the Linux backend and the network-facing decoder” is over. Pure Rust audio device I/O — sovereign, safe, and FFI-free.

KitaSan at COOLJAPAN OÜ August 6, 2026

↑ Back to all posts