Skip to content

Unsafe Code Policy

On this page

Status

Accepted

Context

A key motivation for the Rust port is eliminating the memory safety vulnerabilities that have affected C Oniguruma (CVE-2019-13224, CVE-2019-19204, CVE-2019-19246, CVE-2019-19012, CVE-2019-13225). However, some C patterns in Oniguruma cannot be expressed in safe Rust without a fundamental redesign that would violate the 1:1 parity goal (ADR-001).

Decision

The codebase permits unsafe under one narrowly scoped pattern only:

The pattern: AST raw pointers (regcomp.rs)

Call nodes (Node::Call) share references to their target group nodes. In C, this is a simple pointer assignment. In Rust, the borrow checker cannot express "multiple nodes referencing the same mutable tree node" without Rc<RefCell<>> or arena allocation -- both of which would require redesigning the entire AST.

These raw pointers are:

  • Set once during parsing (prs_call)
  • Never freed independently (the AST owns all nodes)
  • Valid for the lifetime of the regex compilation

No longer needed: global function pointer storage

Earlier revisions allowed a second pattern: global callout callbacks stored as AtomicPtr and read back with transmute. They now live in an AtomicBool plus a RwLock<Option<fn>>, which costs one atomic load when no callout is set, as the pointer did. That pattern is retired, and no new code may reintroduce it.

What is NOT allowed

No unsafe blocks for:

  • Buffer arithmetic or bounds-skipping
  • Memory allocation or deallocation
  • String processing or encoding conversion
  • Transmuting any type, including function pointers

These are precisely the areas where C Oniguruma's CVEs occurred.

Current State

The figures in this section come from scripts/count-unsafe.sh, run from the repository root; they are not kept by hand. Run the script after changing unsafe code and update this section from its output. At this revision it reports:

  • 10 unsafe sites in src/: 4 unsafe blocks and 6 unsafe impl items, with no unsafe fn items.
  • A line total of 60,376 for the same files, so the sites are 0.02% of lines.

The script excludes src/ffi.rs (18 sites, for the benchmark-only ffi feature) and the generated Unicode tables in src/unicode/ (24,588 lines). It prints both separately.

All 10 sites belong to the one pattern:

  • The 4 blocks in regcomp.rs dereference the links from a call to its target group (target_node) and from a back-reference to its group (mem_node), to read the target's length or literal tail.
  • The 6 items are unsafe impl Send and unsafe impl Sync for Node, MemEnv and UnsetAddr in src/regparse_types.rs. Their raw pointers (parent, target_node, mem_node, empty_repeat_node, target) are non-owning links between nodes of one AST.

Issue #267 removed the 51 other sites without a measurable cost: list walks over the parse tree now borrow instead of using raw pointers, the parser reaches the regex under construction through its ParseEnv instead of a pointer, the callout arguments borrow the regex and their data, and pointer arithmetic is gone. Removing the remaining sites would need an AST that refers to nodes by index instead of by pointer.

Consequences

  • The port eliminates buffer over-read/write, use-after-free, double-free, NULL dereference, and uninitialized memory vulnerabilities structurally.
  • The remaining unsafe blocks should be reviewed periodically. If Rust's type system evolves (e.g. better support for self-referential structs), these could potentially be eliminated.
  • Any new unsafe block requires explicit justification and must fall into the permitted pattern.