The last piece of FreeType most “Pure Rust” font stacks still quietly keep around is the hinting bytecode interpreter.
Today we released OxiFont 0.2.1 — adding oxifont-hinting, a from-scratch, bounds-checked Pure Rust implementation of the TrueType instruction set, alongside a WOFF2 spec-compliance fix and a closed large-allocation denial-of-service in bundled CJK font resolution.
No FreeType. No hand-rolled unsafe stack machine trusting font bytes it hasn’t validated. oxifont-hinting runs entirely under #![forbid(unsafe_code)], and every stack, storage, CVT, point, and jump access it makes is bounds-checked against a typed error rather than a segfault. It compiles to a single static binary and runs anywhere Rust does — no C toolchain, no -sys crate, no build script vendoring a copy of FreeType.
Why OxiFont 0.2.1 is a game changer
Most “Pure Rust” font stacks parse TrueType tables just fine but stop short of one thing:
- They parse
glyf/loca/cmapin safe Rust, then either skip hinting entirely or hand it off to the OS rasterizer. - The handful that do execute hinting bytecode typically wrap FreeType’s C interpreter through FFI — quietly re-importing the exact dependency the rest of the stack worked to remove.
- FreeType’s own bytecode interpreter has a real CVE history precisely because a font’s
fpgm/prep/glyph programs are untrusted, attacker-controlled bytecode with jumps, loops, and function calls. - Without bounded execution, a hostile font can loop forever, recurse past the native stack, or walk off the end of the interpreter’s storage/CVT arrays.
OxiFont 0.2.1 ends all of that:
oxifont-hinting, a full instruction-set interpreter.HintingEnginerunsfpgmonce at construction, rescales the CVT and rerunsprepon everyset_ppem, and grid-fits a glyph’s contour plus its four phantom points viahint_glyph— covering the complete TrueType opcode set (vector setup, point movement, delta exceptions,IUPinterpolation, control flow, storage/CVT read-write, and more) across 12 source files, ~4,300 SLOC.- Never panics, by construction. Instruction count, call depth, stack size, loop-counter, and composite-nesting depth are all bounded; every out-of-range access returns a typed
HintingErrorinstead of touching memory it shouldn’t. - Validated against real fonts. A dedicated integration suite runs the VM across the bundled Noto fonts at multiple ppem sizes and stress-tests full glyph sets — 58 tests and 1 doctest passing, 0 failed.
- A WOFF2 spec-compliance fix. The control-byte-
254branch ofRead255UShortdecoding was reading a 3-byte form (a big-endianu16+ 506) instead of the spec’s 2-byte form (one byte + 506) per WOFF2 §5.1 — fixed inoxifont-webfont. - A closed WOFF2 denial-of-service.
reconstruct_glyf_locanow rejects a transformed glyf block whosenPointsStreamclaims more points thanflagStreamcan actually hold — checked before any point-sized buffer is allocated, closing a multi-gigabyte-allocation DoS from a crafted font. - Real bundled CJK fonts.
bundled-noto-cjk-{jp,kr,sc,tc}no longer ship zero-byte placeholders; each now stages a real, developer-supplied TTF at build time, validated as a genuine SFNT.
Technical Deep Dive: the hinting VM, layer by layer
- Font/CVT loading (
font.rs).FontProgram::loadpullsfpgm/prep/cvt/hhea/hmtxout of aSfntTableMap— all optional; a glyf font with none of them is simply unhinted, not an error. - Program execution (
interp.rs).HintingEngine::newruns the font program once, collectingFDEF/IDEFfunction and instruction definitions;set_ppemrescales the CVT from font units and rerunsprepfrom a clean slate on every size change. - Full opcode dispatch (
dispatch.rs,ops_arith.rs,ops_move.rs,ops_state.rs). Vector setup, point/line intersection, rounding-state selection, stack manipulation, point movement (MDAP/MDRP/MIRP/MSIRP/SHP/SHZ/…), storage/CVT read-write, arithmetic, comparisons, and delta exceptions — the complete instruction set, not a subset. IUPinterpolation (ops_iup.rs). Per-contour, per-axis interpolation of points the glyph program left untouched, measured against original vs. current positions.
Getting Started
cargo add oxifont-hinting
use oxifont_core::sfnt::SfntTableMap;
use oxifont_hinting::HintingEngine;
fn run(font_bytes: &[u8]) -> Result<(), oxifont_hinting::HintingError> {
let map = SfntTableMap::parse(font_bytes).map_err(oxifont_hinting::HintingError::from)?;
let mut engine = HintingEngine::new(&map)?;
engine.set_ppem(16)?;
let glyph = engine.hint_glyph(36)?; // grid-fit glyph id 36 at 16 ppem
for cmd in glyph.to_outline() {
// feed `cmd` to a rasterizer …
let _ = cmd;
}
Ok(())
}
oxifont-hinting is not yet re-exported from the oxifont facade crate — depend on it directly for now.
What’s New in 0.2.1
- New crate:
oxifont-hinting— Pure Rust TrueType bytecode hinting interpreter (grid-fitting VM);#![forbid(unsafe_code)]; not yet re-exported from the facade. oxifont-bundled: real build-time CJK fonts —bundled-noto-cjk-{jp,kr,sc,tc}now stage a real SFNT via theOXIFONT_NOTO_CJK_<LANG>environment variable or an in-treefonts/cjk-<lang>/file, validated bybuild.rs, instead of shipping zero-byte placeholders.- Breaking: the CJK accessors are now
noto_sans_<lang>_regular() -> Result<&'static [u8], FontError>instead ofpub static ...: &[u8]. - Fixed: WOFF2
Read255UShortcontrol-byte-254decoding (oxifont-webfont) — now spec-correct per §5.1. - Fixed / Security:
reconstruct_glyf_locarejects an oversizednPointsStreambefore allocating point buffers, closing a large-allocation DoS. oxifont-subset::cmap::build_format4now returnsResult, rejecting subsets that would overflow theu16segment-count boundary instead of risking silent overflow.- Removed
benches/woff2_compare.rsand its non-optionalbrotlidev-dependency chain (deny.tomlcompliance). - Dependency bumps:
oxiarc-deflate/oxiarc-brotli0.3.3 → 0.4.0,quick-xml0.40.1 → 0.41.0,memmap20.9.10 → 0.9.11,oxicode0.2.4 → 0.2.5. - 1,020 tests passing, 0 failed, 2 skipped with
--all-features(962 passing under default features); 11 crates in the workspace, ~34,500 Rust SLOC.
Tips
- Depend on
oxifont-hintingdirectly. It is intentionally not re-exported from theoxifontfacade yet — add it alongsideoxifontif you need grid-fitted outlines. - Reuse one
HintingEngineacross ppem changes. Construction runsfpgmonce; callset_ppemagain whenever the target size changes rather than rebuilding the engine — it rescales the CVT and rerunsprepfor you. - A font with no hinting tables is not an error. If
fpgm/prep/cvtare absent,FontProgram::loadjust treats the glyph as unhinted — check for that instead of assuming every font ships instructions. - If you build with
bundled-noto-cjk-{jp,kr,sc,tc}, supply a real font. PointOXIFONT_NOTO_CJK_<LANG>at a TTF or drop one intofonts/cjk-<lang>/before building —build.rsnow validates the SFNT magic and fails the build on garbage input rather than silently bundling an empty placeholder. - Re-encode WOFF2 fonts touched by third-party encoders. If you decode WOFF2 produced by another encoder that emits the
254-control-byte form ofRead255UShort, upgrade to 0.2.1 — earlier versions could misparse and desync the byte stream on that specific form. - Handle the new
Resultfromoxifont-subset’sbuild_format4. Large-but-valid subsets near theu16segment-count boundary now return a typedSubsetError::InvalidFontinstead of ever risking a silently corrupt cmap table.
This is the foundation
OxiFont is the font foundation under OxiText (glyph metrics for layout), oxigaf (PDF CFF/Type-0 font embedding), oximedia (subtitle and OSD rendering), oxigdal-symbology (map labels), oxiphoton (image text overlay), and OxiUI (GUI text rendering) — none of them need a code change to pick up this release, only a version bump.
Repository: https://github.com/cool-japan/oxifont
Star the repo if you want TrueType hinting bytecode executed without ever linking FreeType.
The era of shipping a C hinting interpreter just to grid-fit outlines at small sizes is over. Pure Rust typography — sovereign, safe, and FFI-free.
— KitaSan at COOLJAPAN OÜ July 30, 2026