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
unsafesites insrc/: 4unsafeblocks and 6unsafe implitems, with nounsafe fnitems. - 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.rsdereference 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 Sendandunsafe impl SyncforNode,MemEnvandUnsetAddrinsrc/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
unsafeblocks 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
unsafeblock requires explicit justification and must fall into the permitted pattern.