I'm not 100% sure of this issue but it seems right.
When returning realistic hygiene mark identifiers, host/guest payload sizes can mismatch, leading to memory corruptions across Wasm plugin execution boundaries.
Full Gemini Codebase Audit Report Appendix
Unsafe Rust Review: swc_common (v19)
Overall Safety Assessment
The swc_common (v19) crate serves as the foundational utility and data structure library for the SWC compiler ecosystem. It encapsulates core compiler concepts including AST byte spans (Span, BytePos), macro hygiene contexts (SyntaxContext, Mark), source map file registries (SourceMap, SourceFile), diagnostic error emission (DiagnosticBuilder), zero-copy cached structures (CacheCell), and guest-host Wasm plugin serialization boundaries (PluginSerializedBytes).
Across the crate, 9 source files contain explicit unsafe blocks, unsafe trait implementations, or unsafe fn declarations. The unsafe surfaces fall into four primary categories:
- String and Slice Indexing: Unchecked string slicing (
str::get_unchecked) in input parsing streams (StringInput).
- Zero-Copy Serialization Deserialization: Place casting (
Place::cast_unchecked) and unchecked reachability hints (unreachable_unchecked) within rkyv archive implementations (CacheCell).
- Foreign Function Interface (FFI) Synchronization: Interacting with native Windows system APIs (
CreateMutexA, WaitForSingleObject) for cross-process compiler lock synchronization.
- Wasm Host-Guest Plugin Boundaries: Raw memory pointer reconstruction (
std::slice::from_raw_parts) and extern imported host proxy calls (__mark_is_descendant_of_proxy, __syntax_context_remove_mark_proxy) used during compiler plugin execution.
Safety documentation across the crate is sparse, with many unsafe blocks lacking formal safety comments or relying on informal notes that omit explicit safety preconditions and invariants.
More severely, auditing these boundaries revealed four critical soundness vulnerabilities. The codebase frequently places unchecked memory-safety preconditions on safe APIs (pub fn, safe trait methods), shifting verification obligations onto safe callers. Most dramatically, the Wasm plugin serialization architecture contains a real heap buffer overflow caused by migrating from fixed-size layout serialization (rkyv) to variable-length CBOR serialization (cbor4ii) without updating fixed buffer preallocation sizing.
Critical Findings
1. Guest Wasm Heap Buffer Overflow in Variable-Length CBOR Host Serialization (src/syntax_pos/hygiene.rs lines 196-216, 241-258, 410-426) 🔴 🚨
- Priority: 🔴 High
- Threat Vector: 🚨 Untrusted Input
- Bug Type:
Heap Buffer Overflow
- Vulnerability: In Wasm plugin execution mode (
cfg(all(feature = "__plugin_mode", target_arch = "wasm32"))), guest methods Mark::is_descendant_of, Mark::least_ancestor, and SyntaxContext::remove_mark communicate with the compiler host by passing raw linear memory pointers. To allocate guest memory for the host return payload, these methods invoke PluginSerializedBytes::try_serialize(&VersionedSerializable::new(MutableMarkContext(0, 0, 0))). This serializes dummy integer values 0 using CBOR (cbor4ii). In CBOR, integers in 0..=23 are encoded as a single byte (0x00). Consequently, try_serialize allocates a backing Vec<u8> buffer of exactly 7 bytes. The raw pointer ptr and capacity len (7) are extracted, and ptr is passed as an i32 memory offset to host FFI proxies (e.g., __mark_is_descendant_of_proxy(self.0, ancestor.0, ptr as _)).
- Exploitation & Corruption: When the host executes the proxy, it computes the actual
MutableMarkContext(self_mark, ancestor, flags) return values and encodes them into CBOR. In CBOR (cbor4ii), integers in 24..=255 require 2 bytes, integers in 256..=65535 require 3 bytes, and large integers up to 4_294_967_295 require 5 bytes (tag 0x1A followed by 4 big-endian bytes). In real compilation pipelines, macro hygiene mark identifiers frequently exceed 65,536. When returning realistic marks (e.g., Mark(100_000)), the CBOR payload expands up to 15 bytes. The host writes this 15-byte payload directly into guest linear memory starting at ptr, overflowing the 7-byte guest Vec<u8> heap allocation and corrupting adjacent Wasm heap allocator metadata and live data structures.
- Secondary Corruption: Following the proxy call, line 210 invokes
PluginSerializedBytes::from_raw_ptr(ptr, len) using the original dummy length (len = 7). Decoding a truncated 7-byte slice of a 15-byte CBOR stream causes cbor4ii deserialization to fail with unexpected EOF errors or silently produce malformed context integers.
- Root Cause: Maintainers previously used
rkyv (fixed repr(C) struct layout) where MutableMarkContext always occupied 12 bytes regardless of field values. When migrating plugin serialization from rkyv to cbor4ii, maintainers retained fixed dummy-value preallocation, overlooking the variable-length nature of CBOR integers.
2. Unsound Safe Wrapper Dereferencing Raw Pointers (src/plugin/serialized.rs lines 98-106) 🔴 ⚠️
-
Priority: 🔴 High
-
Threat Vector: ⚠️ Accidental Misuse
-
Bug Type: Soundness Violation
-
Vulnerability: PluginSerializedBytes::from_raw_ptr(raw_allocated_ptr: *const u8, raw_allocated_ptr_len: usize) -> PluginSerializedBytes is declared as a safe public function. Internally, line 103 executes unsafe { std::slice::from_raw_parts(raw_allocated_ptr, raw_allocated_ptr_len) }.
-
Violation: Safe API surfaces must not require callers to uphold unchecked memory-safety preconditions without declaring the function unsafe. Safe caller code can invoke PluginSerializedBytes::from_raw_ptr(std::ptr::null(), 1000) or supply dangling pointers, causing immediate Undefined Behavior. The author recognized that from_raw_ptr dereferences a raw pointer and included an #[allow(clippy::not_unsafe_ptr_arg_deref)] attribute. To fully encapsulate memory safety, marking from_raw_ptr as pub unsafe fn with a # Safety docstring will make caller requirements explicit.
3. Safe Trait Method Imposing Unchecked Slicing Preconditions Leading to UTF-8 Slicing UB (src/input.rs lines 300-314, 219-227) 🔴 ⚠️
-
Priority: 🔴 High
-
Threat Vector: ⚠️ Accidental Misuse
-
Bug Type: Invalid UTF-8 Slicing
-
Vulnerability: Trait Input<'a> defines fn eat_byte(&mut self, c: u8) -> bool as a safe method. The doc comment attaches a prose rule: "c must be ASCII". The default trait implementation calls self.is_byte(c) and then unsafe { self.bump_bytes(1) }. For StringInput, bump_bytes(1) executes self.remaining = unsafe { self.remaining.get_unchecked(1..) }.
-
Violation: If safe caller code invokes input.eat_byte(0xC3) on a StringInput whose remaining text begins with a 2-byte UTF-8 character such as "é" (UTF-8 bytes [0xC3, 0xA9]), is_byte(0xC3) matches the leading byte 0xC3. eat_byte then advances the string slice by exactly 1 byte via get_unchecked(1..). This slices &str at byte index 1 (between 0xC3 and 0xA9), which is not a UTF-8 code point boundary. Creating a &str reference pointing to invalid UTF-8 violates core language rules for UTF-8 slices, triggering immediate Undefined Behavior. The author provided an inline safety note (// Safety: We are sure that the input is not empty), correctly observing that the input is non-empty. Expanding this note to also verify that bump_bytes lands on valid UTF-8 code point boundaries (e.g. by checking c.is_ascii()) will make the invariant self-documenting for future maintainers.
4. Safe Trait Method Triggering UB via unreachable_unchecked() on Cache Miss (src/cache.rs lines 64-93) 🔴 ⚠️
-
Priority: 🔴 High
-
Threat Vector: ⚠️ Accidental Misuse
-
Bug Type: Unreachable Code Execution
-
Vulnerability: CacheCell<T> implements rkyv::Archive. The safe trait method Archive::resolve(&self, resolver: Self::Resolver, out: Place<Self::Archived>) matches on resolver: Option<T::Resolver>. In the Some(resolver) branch, line 81 checks if let Some(value) = self.get(). If self.get() returns None, line 85 executes unsafe { unreachable_unchecked(); }.
-
Violation: Archive::resolve is a safe trait method. Safe caller code can instantiate an empty CacheCell::new() (where self.get() returns None), obtain or forge a Some(resolver) (e.g., Some(()) for types where T::Resolver = ()), and invoke cache_cell.resolve(Some(()), out). This directly triggers unreachable_unchecked() from safe Rust code, causing Undefined Behavior.
Fishy Findings
1. Incomplete # Safety Contracts on String Slicing Constructors (src/input.rs lines 53-68, 270-274) 🟠 ⚠️
-
Priority: 🟠 Medium
-
Threat Vector: ⚠️ Accidental Misuse
-
Bug Type: Incomplete Safety Documentation
-
Issue: unsafe fn slice_str(&self, start: BytePos, end: BytePos) and unsafe fn slice(&mut self, start: BytePos, end: BytePos) state # Safety preconditions requiring start and end to be in the "valid range of input". However, they omit the critical memory-safety requirement that start and end must lie on valid UTF-8 code point boundaries of self.orig. If caller code passes byte positions falling inside multi-byte characters, get_unchecked produces invalid UTF-8. Furthermore, StringInput::new(src, start, end) is a safe constructor accepting arbitrary BytePos values without verifying that (end - start) == src.len(), enabling safe callers to desynchronize internal index offsets and cause integer wraparound in (start - self.orig_start).0 as usize.
2. Anonymous # Safety Obligations Without Naming Responsible Parties (src/input.rs lines 53, 243, 270, 282) 🟡 ⚠️
-
Priority: 🟡 Low
-
Threat Vector: ⚠️ Accidental Misuse
-
Bug Type: Ambiguous Safety Contract
-
Issue: # Safety docstrings on slice_str, bump_bytes, slice, and reset_to use passive or anonymous phrasing (e.g., - start should be less than or equal to end). Safety documentation should explicitly specify the party (caller, implementer, linker, or host environment) responsible for meeting safety preconditions.
3. Informal Comment Formatting on UTF-8 Source Map Identifiers (src/source_map.rs lines 1298-1301, 1411-1414) 🟡 ⚠️
-
Priority: 🟡 Low
-
Threat Vector: ⚠️ Accidental Misuse
-
Bug Type: Informal Safety Comment
-
Issue: Adjacent to BytesStr::from_utf8_slice_unchecked(name.as_bytes()), the author included a helpful inline note (// Safety: name is &str, so it's valid UTF-8), correctly identifying that &str guarantees UTF-8 validity. Expanding this comment to explicitly document the &str UTF-8 invariant and standardizing the safety comment format will enhance maintainability.
Missing Safety Comments
Below are exact file:line locations where // SAFETY: comments or # Safety docstrings are missing or defective, along with rigorous proposed proof comments:
src/cache.rs:71 🔴
// SAFETY:
// Operation: `Place::cast_unchecked::<ArchivedOptionVariantNone>(out)`.
// Required contract: The destination type `ArchivedOptionVariantNone` must fit within the memory allocated for `out: Place<ArchivedOption<T::Archived>>` and satisfy alignment requirements.
// Evidence:
// - `Dependency Invariant`: `ArchivedOptionVariantNone` is a `repr(C)` single-field wrapper around `ArchivedOptionTag` (`repr(u8)`), occupying exactly 1 byte with alignment 1.
// - `Dependency Invariant`: `ArchivedOption<T::Archived>` contains tag `ArchivedOptionTag` as its discriminant, ensuring its layout size is at least 1 byte and alignment is at least 1.
// - `Local Invariant`: `out` points to an allocated place valid for `ArchivedOption<T::Archived>`.
// Therefore casting `out` to `ArchivedOptionVariantNone` is within allocation bounds and preserves alignment.
src/cache.rs:77 🔴
// SAFETY:
// Operation: `Place::cast_unchecked::<ArchivedOptionVariantSome<T::Archived>>(out)`.
// Required contract: `ArchivedOptionVariantSome<T::Archived>` must fit within `out: Place<ArchivedOption<T::Archived>>` and satisfy alignment requirements.
// Evidence:
// - `Dependency Invariant`: `ArchivedOption<U>` in `rkyv` is layout-compatible with `ArchivedOptionVariantSome<U>` (`repr(C)` tag + payload `U`) for the `Some` variant.
// - `Local Invariant`: `out` points to an uninitialized place allocated for `ArchivedOption<T::Archived>`.
// Therefore the unchecked cast is valid for writing the `Some` variant representation.
src/cache.rs:84 🔴
(Note: As detailed in Critical Findings, this operation is unsound in safe trait method Archive::resolve and must be replaced with a safe panic or safe error handling. If retained behind an internal invariant check enforced by sealed constructors):
// SAFETY:
// Operation: `std::hint::unreachable_unchecked()`.
// Required contract: Control flow must never reach this program point.
// Evidence:
// - `INVARIANT`: For this instance, `resolver` is `Some` only if `self.get()` was previously populated during `serialize()`.
// - `Local Invariant`: `OnceCell` monotonic liveness ensures once populated, `self.get()` remains `Some`.
src/input.rs:65 🔴
// SAFETY:
// Operation: `str::get_unchecked(s, start_idx..end_idx)`.
// Required contract: `start_idx <= end_idx <= s.len()`, and both `start_idx` and `end_idx` must lie on UTF-8 character boundaries of `s`.
// Evidence:
// - `Precondition`: By this function's `# Safety` contract, `start <= end`, ensuring `start_idx <= end_idx`.
// - `Precondition`: By the same contract, `start` and `end` are within valid range and lie on UTF-8 code point boundaries of `s`.
// - `Local Invariant`: `debug_assert!(end_idx <= s.len())` checks upper bound liveness.
// Therefore `get_unchecked` slices on valid UTF-8 boundaries within `s`.
src/input.rs:110 🔴
// SAFETY:
// Operation: `str::get_unchecked(self.remaining, n..)`.
// Required contract: `n <= self.remaining.len()`, and `n` must lie on a UTF-8 character boundary.
// Evidence:
// - `Precondition`: By `bump_bytes`'s `# Safety` contract, `n` is the exact byte count of the current UTF-8 character at index 0.
// - `Precondition`: Slicing a valid UTF-8 string at the end of its first character lands exactly on the next UTF-8 character boundary (or string end).
// - `Local Invariant`: `debug_assert!(n <= self.remaining.len())` confirms bounds.
// Therefore `get_unchecked(n..)` produces a valid UTF-8 slice.
src/input.rs:155 and src/input.rs:157 🔴
// SAFETY:
// Operation: `str::get_unchecked(s, start_idx..end_idx)` and `str::get_unchecked(s, end_idx..)`.
// Required contract: `start_idx <= end_idx <= s.len()`, and indices must lie on UTF-8 boundaries.
// Evidence:
// - `Precondition`: By `slice`'s `# Safety` contract, `start <= end` and both positions correspond to valid UTF-8 boundaries in `s`.
// - `Local Invariant`: `debug_assert!(end_idx <= s.len())` verifies range liveness.
// Therefore both unchecked slice operations preserve UTF-8 validity.
src/input.rs:181 and src/input.rs:184 🔴
// SAFETY:
// Operation: `str::get_unchecked(self.remaining, ..last)` and `str::get_unchecked(self.remaining, last..)`.
// Required contract: `last <= self.remaining.len()`, and `last` must lie on a UTF-8 boundary.
// Evidence:
// - `Precondition`: `self.remaining` is `&str`, guaranteed valid UTF-8.
// - `Local Invariant`: `last` is computed by summing `c.len_utf8()` for consecutive `char` elements yielded by `self.remaining.chars()`.
// - `Precondition`: Summing full UTF-8 character lengths from index 0 strictly lands on valid UTF-8 code point boundaries.
// - `Local Invariant`: `debug_assert!(last <= self.remaining.len())` confirms bound.
// Therefore both prefix and suffix unchecked slices are sound.
src/input.rs:200 🔴
// SAFETY:
// Operation: `str::get_unchecked(orig, idx..)`.
// Required contract: `idx <= orig.len()`, and `idx` must lie on a UTF-8 character boundary.
// Evidence:
// - `Precondition`: By `reset_to`'s `# Safety` contract, `to` is a valid position corresponding to a UTF-8 character boundary in `orig`.
// - `Local Invariant`: `debug_assert!(idx <= orig.len())` confirms bounds.
// Therefore slicing `orig` from `idx` is sound.
src/input.rs:221 🔴
(Note: As detailed in Critical Findings, this is unsound for non-ASCII c. Subject to fixing eat_byte by adding c.is_ascii() validation):
// SAFETY:
// Operation: `str::get_unchecked(self.remaining, 1..)`.
// Required contract: `1 <= self.remaining.len()`, and index `1` must lie on a UTF-8 character boundary.
// Evidence:
// - `Local Invariant`: `self.is_byte(c)` verified that `self.remaining` has at least 1 byte and its first byte equals `c`.
// - `Local Invariant`: `c.is_ascii()` (required fix) ensures `c <= 0x7F`.
// - `Precondition`: In UTF-8, any leading byte `<= 0x7F` represents a complete 1-byte ASCII character.
// Therefore index `1` is exactly the end of the first UTF-8 character and a valid slice boundary.
src/syntax_pos.rs:648 🔴
// SAFETY:
// Operation: FFI call `__span_dummy_with_cmt_proxy()`.
// Required contract: The foreign Wasm host environment must provide an ABI-compatible implementation of `__span_dummy_with_cmt_proxy` that does not unwind across the FFI boundary.
// Evidence:
// - `Dependency Invariant`: In Wasm plugin execution mode (`__plugin_mode`), the SWC host runtime guarantees linking this symbol.
// - `Type Invariant`: The return type `u32` has no validity invariants; any returned Wasm integer is valid.
// Postcondition: Returns a dummy span position index.
src/syntax_pos/hygiene.rs:151, 183, 204, 247, 382, 415, 461 🔴
(Note: For is_descendant_of line 204, least_ancestor line 247, and remove_mark line 415, the CBOR buffer preallocation sizing vulnerability described in Critical Findings must be fixed):
// SAFETY (for lines 151, 183, 382, 461 - scalar proxies):
// Operation: Invocation of Wasm host FFI proxy function.
// Required contract: Host runtime must provide ABI-compatible symbol without FFI unwinding.
// Evidence:
// - `Dependency Invariant`: SWC plugin runner environment resolves proxied symbols.
// - `Type Invariant`: Scalar `u32` return values contain no uninit bits or reference invariants.
// SAFETY (for lines 204, 247, 415 - memory buffer proxies):
// Operation: FFI call passing raw guest memory pointer `ptr as i32`.
// Required contract: Host runtime must write valid CBOR bytes into `ptr`, not exceeding allocated capacity `len`.
// Evidence:
// - `Local Invariant`: `serialized.as_ptr()` provides pointer `ptr` and capacity `len` to a live guest `Vec<u8>`.
// - `Dependency Invariant`: Host SWC compiler contract guarantees writing CBOR payload within agreed capacity bounds (subject to fixing the Critical Finding overflow).
// Postcondition: Guest memory at `ptr..ptr + len` contains valid CBOR bytes ready for safe decoding.
src/plugin/serialized.rs:103 🔴
(Note: As established in Critical Findings, from_raw_ptr must be promoted to pub unsafe fn):
// SAFETY:
// Operation: `std::slice::from_raw_parts(raw_allocated_ptr, raw_allocated_ptr_len)`.
// Required contract: `raw_allocated_ptr` must be non-null, properly aligned for `u8` (alignment 1), valid for reads of `raw_allocated_ptr_len` bytes, and point to initialized memory within a single live allocation.
// Evidence:
// - `Precondition`: By this function's `# Safety` contract (required `unsafe fn` promotion), caller ensures `raw_allocated_ptr` points to `raw_allocated_ptr_len` readable bytes in a live Wasm allocation.
// - `Type Invariant`: Alignment for `u8` is 1, satisfied by any address.
// Postcondition: Produces immutable slice `&[u8]` valid for the duration of the call.
src/errors/lock.rs:58, 68, 75 🔴
// SAFETY (line 58):
// Operation: `CloseHandle(self.0)`.
// Required contract: `self.0` must be a valid open Windows object handle.
// Evidence:
// - `INVARIANT`: `Handle` is constructed only from non-null handles returned by `CreateMutexA`.
// - `Local Invariant`: `Drop` runs exactly once, ensuring handle is not double-closed.
// SAFETY (line 68):
// Operation: `ReleaseMutex((self.0).0)`.
// Required contract: The calling thread must currently hold ownership of the mutex handle.
// Evidence:
// - `Local Invariant`: `Guard` is created immediately after `WaitForSingleObject` returns `WAIT_OBJECT_0` or `WAIT_ABANDONED`, transferring mutex ownership to this thread.
// - `Local Invariant`: `Guard` is `!Send` and `!Sync`, ensuring `Drop` executes on the exact thread that acquired the mutex.
// SAFETY (line 75):
// Operation: `CreateMutexA(null_mut(), 0, cname.as_ptr() as *const u8)`.
// Required contract: `lpName` must be a valid null-terminated C string.
// Evidence:
// - `Local Invariant`: `cname` was constructed via `CString::new(name).unwrap()`, guaranteeing a valid null-terminated ASCII/ANSI buffer.
src/errors/diagnostic_builder.rs:178 🟠
// SAFETY:
// Operation: `std::ptr::read(&self.diagnostic)` and `std::mem::forget(self)`.
// Required contract: `&self.diagnostic` must be valid for reads of `Box<Diagnostic>`, properly aligned, and point to initialized memory. The duplicated ownership of `Box<Diagnostic>` must not be double-dropped.
// Evidence:
// - `Local Invariant`: `self` is a live, immutably/mutably borrowed `DiagnosticBuilder` containing an initialized `Box<Diagnostic>`.
// - `Local Invariant`: `std::mem::forget(self)` prevents `DiagnosticBuilder::drop` from executing on `self`.
// - `Type Invariant`: `self.handler` is a shared reference and `allow_suggestions` is `bool`; neither has drop logic.
// Postcondition: Ownership of `Box<Diagnostic>` is cleanly moved into local `diagnostic` without triggering destructor bombs or double-frees.
src/syntax_pos/analyze_source_file.rs:67 🟡
// SAFETY:
// Operation: `slice::get_unchecked(src_bytes, i)`.
// Required contract: `i < src_bytes.len()`.
// Evidence:
// - `Local Invariant`: `assert!(src.len() >= scan_len)` and loop condition `while i < scan_len` ensure `i < scan_len <= src.len()`.
// - `Type Invariant`: `src_bytes.len() == src.len()`.
// Therefore index `i` is strictly in bounds of `src_bytes`.
Note
This finding was identified during an agentic unsafe Rust code review performed by Gemini AI, followed by human review and verification.
I'm not 100% sure of this issue but it seems right.
The Issue
In Wasm plugin execution mode (
cfg(all(feature = "plugin-mode", target_arch = "wasm32"))), guest methodsMark::is_descendant_ofandMark::least_ancestorcommunicate with the compiler host by passing raw linear memory pointers. To allocate guest memory for the host return payload, these methods invokePluginSerializedBytes::try_serialize(&VersionedSerializable::new(MutableMarkContext(0, 0, 0))),serializing dummy integer values0usingrkyv.swc/crates/swc_common/src/syntax_pos/hygiene.rs
Lines 182 to 185 in 2bd5b44
swc/crates/swc_common/src/syntax_pos/hygiene.rs
Lines 224 to 227 in 2bd5b44
When returning realistic hygiene mark identifiers, host/guest payload sizes can mismatch, leading to memory corruptions across Wasm plugin execution boundaries.
I believe this code is where the actual host/guest copy happens:
swc/crates/swc_plugin_runner/src/memory_interop.rs
Lines 52 to 55 in 3298cb7
Maintainers previously used
rkyv(fixedrepr(C)struct layout) whereMutableMarkContextoccupied fixed bytes regardless of field values. Note: Inswc_commonv0.23.0,eat_byteUTF-8 slicing was subsequently updated/hardened in commit669a659c6e(fix(swc_common): make eat_byte unsafe to prevent UTF-8 boundary violation).Minimal Reproduction
Miri can't plug into a wasm runtime, so this just shows off the mismatch between the prealloc and the payload.
Suggested Fix
When preallocating buffers for Wasm plugin guest return payloads encoded via CBOR, the preallocated capacity must account for the maximum possible CBOR variable-length encoding size of
MutableMarkContext(whereu32integers up tou32::MAXoccupy 5 bytes each, totaling up to 17 bytes), rather than serializing dummy0values. Alternatively,PluginSerializedBytesshould allocate a fixed upper-bound capacity (e.g. 32 bytes) or use fixed-size representation.Note
The full audit report below also contains additional minor findings (such as missing safety comments or undocumented FFI assumptions) that are probably worth fixing as well but not the primary goal of this issue. The audit report has not been human-reviewed, it may contain misleading claims.
Full Gemini Codebase Audit Report Appendix
Unsafe Rust Review:
swc_common(v19)Overall Safety Assessment
The
swc_common(v19) crate serves as the foundational utility and data structure library for the SWC compiler ecosystem. It encapsulates core compiler concepts including AST byte spans (Span,BytePos), macro hygiene contexts (SyntaxContext,Mark), source map file registries (SourceMap,SourceFile), diagnostic error emission (DiagnosticBuilder), zero-copy cached structures (CacheCell), and guest-host Wasm plugin serialization boundaries (PluginSerializedBytes).Across the crate, 9 source files contain explicit
unsafeblocks,unsafe traitimplementations, orunsafe fndeclarations. Theunsafesurfaces fall into four primary categories:str::get_unchecked) in input parsing streams (StringInput).Place::cast_unchecked) and unchecked reachability hints (unreachable_unchecked) withinrkyvarchive implementations (CacheCell).CreateMutexA,WaitForSingleObject) for cross-process compiler lock synchronization.std::slice::from_raw_parts) and extern imported host proxy calls (__mark_is_descendant_of_proxy,__syntax_context_remove_mark_proxy) used during compiler plugin execution.Safety documentation across the crate is sparse, with many
unsafeblocks lacking formal safety comments or relying on informal notes that omit explicit safety preconditions and invariants.More severely, auditing these boundaries revealed four critical soundness vulnerabilities. The codebase frequently places unchecked memory-safety preconditions on safe APIs (
pub fn, safe trait methods), shifting verification obligations onto safe callers. Most dramatically, the Wasm plugin serialization architecture contains a real heap buffer overflow caused by migrating from fixed-size layout serialization (rkyv) to variable-length CBOR serialization (cbor4ii) without updating fixed buffer preallocation sizing.Critical Findings
1. Guest Wasm Heap Buffer Overflow in Variable-Length CBOR Host Serialization (
src/syntax_pos/hygiene.rslines 196-216, 241-258, 410-426) 🔴 🚨Heap Buffer Overflowcfg(all(feature = "__plugin_mode", target_arch = "wasm32"))), guest methodsMark::is_descendant_of,Mark::least_ancestor, andSyntaxContext::remove_markcommunicate with the compiler host by passing raw linear memory pointers. To allocate guest memory for the host return payload, these methods invokePluginSerializedBytes::try_serialize(&VersionedSerializable::new(MutableMarkContext(0, 0, 0))). This serializes dummy integer values0using CBOR (cbor4ii). In CBOR, integers in0..=23are encoded as a single byte (0x00). Consequently,try_serializeallocates a backingVec<u8>buffer of exactly 7 bytes. The raw pointerptrand capacitylen(7) are extracted, andptris passed as ani32memory offset to host FFI proxies (e.g.,__mark_is_descendant_of_proxy(self.0, ancestor.0, ptr as _)).MutableMarkContext(self_mark, ancestor, flags)return values and encodes them into CBOR. In CBOR (cbor4ii), integers in24..=255require 2 bytes, integers in256..=65535require 3 bytes, and large integers up to4_294_967_295require 5 bytes (tag0x1Afollowed by 4 big-endian bytes). In real compilation pipelines, macro hygiene mark identifiers frequently exceed 65,536. When returning realistic marks (e.g.,Mark(100_000)), the CBOR payload expands up to 15 bytes. The host writes this 15-byte payload directly into guest linear memory starting atptr, overflowing the 7-byte guestVec<u8>heap allocation and corrupting adjacent Wasm heap allocator metadata and live data structures.PluginSerializedBytes::from_raw_ptr(ptr, len)using the original dummy length (len = 7). Decoding a truncated 7-byte slice of a 15-byte CBOR stream causescbor4iideserialization to fail with unexpected EOF errors or silently produce malformed context integers.rkyv(fixedrepr(C)struct layout) whereMutableMarkContextalways occupied 12 bytes regardless of field values. When migrating plugin serialization fromrkyvtocbor4ii, maintainers retained fixed dummy-value preallocation, overlooking the variable-length nature of CBOR integers.2. Unsound Safe Wrapper Dereferencing Raw Pointers (⚠️
src/plugin/serialized.rslines 98-106) 🔴Priority: 🔴 High
Threat Vector:⚠️ Accidental Misuse
Bug Type:
Soundness ViolationVulnerability:
PluginSerializedBytes::from_raw_ptr(raw_allocated_ptr: *const u8, raw_allocated_ptr_len: usize) -> PluginSerializedBytesis declared as a safe public function. Internally, line 103 executesunsafe { std::slice::from_raw_parts(raw_allocated_ptr, raw_allocated_ptr_len) }.Violation: Safe API surfaces must not require callers to uphold unchecked memory-safety preconditions without declaring the function
unsafe. Safe caller code can invokePluginSerializedBytes::from_raw_ptr(std::ptr::null(), 1000)or supply dangling pointers, causing immediate Undefined Behavior. The author recognized thatfrom_raw_ptrdereferences a raw pointer and included an#[allow(clippy::not_unsafe_ptr_arg_deref)]attribute. To fully encapsulate memory safety, markingfrom_raw_ptraspub unsafe fnwith a# Safetydocstring will make caller requirements explicit.3. Safe Trait Method Imposing Unchecked Slicing Preconditions Leading to UTF-8 Slicing UB (⚠️
src/input.rslines 300-314, 219-227) 🔴Priority: 🔴 High
Threat Vector:⚠️ Accidental Misuse
Bug Type:
Invalid UTF-8 SlicingVulnerability: Trait
Input<'a>definesfn eat_byte(&mut self, c: u8) -> boolas a safe method. The doc comment attaches a prose rule: "cmust be ASCII". The default trait implementation callsself.is_byte(c)and thenunsafe { self.bump_bytes(1) }. ForStringInput,bump_bytes(1)executesself.remaining = unsafe { self.remaining.get_unchecked(1..) }.Violation: If safe caller code invokes
input.eat_byte(0xC3)on aStringInputwhose remaining text begins with a 2-byte UTF-8 character such as"é"(UTF-8 bytes[0xC3, 0xA9]),is_byte(0xC3)matches the leading byte0xC3.eat_bytethen advances the string slice by exactly 1 byte viaget_unchecked(1..). This slices&strat byte index 1 (between0xC3and0xA9), which is not a UTF-8 code point boundary. Creating a&strreference pointing to invalid UTF-8 violates core language rules for UTF-8 slices, triggering immediate Undefined Behavior. The author provided an inline safety note (// Safety: We are sure that the input is not empty), correctly observing that the input is non-empty. Expanding this note to also verify thatbump_byteslands on valid UTF-8 code point boundaries (e.g. by checkingc.is_ascii()) will make the invariant self-documenting for future maintainers.4. Safe Trait Method Triggering UB via⚠️
unreachable_unchecked()on Cache Miss (src/cache.rslines 64-93) 🔴Priority: 🔴 High
Threat Vector:⚠️ Accidental Misuse
Bug Type:
Unreachable Code ExecutionVulnerability:
CacheCell<T>implementsrkyv::Archive. The safe trait methodArchive::resolve(&self, resolver: Self::Resolver, out: Place<Self::Archived>)matches onresolver: Option<T::Resolver>. In theSome(resolver)branch, line 81 checksif let Some(value) = self.get(). Ifself.get()returnsNone, line 85 executesunsafe { unreachable_unchecked(); }.Violation:
Archive::resolveis a safe trait method. Safe caller code can instantiate an emptyCacheCell::new()(whereself.get()returnsNone), obtain or forge aSome(resolver)(e.g.,Some(())for types whereT::Resolver = ()), and invokecache_cell.resolve(Some(()), out). This directly triggersunreachable_unchecked()from safe Rust code, causing Undefined Behavior.Fishy Findings
1. Incomplete⚠️
# SafetyContracts on String Slicing Constructors (src/input.rslines 53-68, 270-274) 🟠Priority: 🟠 Medium
Threat Vector:⚠️ Accidental Misuse
Bug Type:
Incomplete Safety DocumentationIssue:
unsafe fn slice_str(&self, start: BytePos, end: BytePos)andunsafe fn slice(&mut self, start: BytePos, end: BytePos)state# Safetypreconditions requiringstartandendto be in the "valid range of input". However, they omit the critical memory-safety requirement thatstartandendmust lie on valid UTF-8 code point boundaries ofself.orig. If caller code passes byte positions falling inside multi-byte characters,get_uncheckedproduces invalid UTF-8. Furthermore,StringInput::new(src, start, end)is a safe constructor accepting arbitraryBytePosvalues without verifying that(end - start) == src.len(), enabling safe callers to desynchronize internal index offsets and cause integer wraparound in(start - self.orig_start).0 as usize.2. Anonymous⚠️
# SafetyObligations Without Naming Responsible Parties (src/input.rslines 53, 243, 270, 282) 🟡Priority: 🟡 Low
Threat Vector:⚠️ Accidental Misuse
Bug Type:
Ambiguous Safety ContractIssue:
# Safetydocstrings onslice_str,bump_bytes,slice, andreset_touse passive or anonymous phrasing (e.g.,- start should be less than or equal to end). Safety documentation should explicitly specify the party (caller, implementer, linker, or host environment) responsible for meeting safety preconditions.3. Informal Comment Formatting on UTF-8 Source Map Identifiers (⚠️
src/source_map.rslines 1298-1301, 1411-1414) 🟡Priority: 🟡 Low
Threat Vector:⚠️ Accidental Misuse
Bug Type:
Informal Safety CommentIssue: Adjacent to
BytesStr::from_utf8_slice_unchecked(name.as_bytes()), the author included a helpful inline note (// Safety: name is &str, so it's valid UTF-8), correctly identifying that&strguarantees UTF-8 validity. Expanding this comment to explicitly document the&strUTF-8 invariant and standardizing the safety comment format will enhance maintainability.Missing Safety Comments
Below are exact file:line locations where
// SAFETY:comments or# Safetydocstrings are missing or defective, along with rigorous proposed proof comments:src/cache.rs:71🔴src/cache.rs:77🔴src/cache.rs:84🔴(Note: As detailed in Critical Findings, this operation is unsound in safe trait method
Archive::resolveand must be replaced with a safe panic or safe error handling. If retained behind an internal invariant check enforced by sealed constructors):src/input.rs:65🔴src/input.rs:110🔴src/input.rs:155andsrc/input.rs:157🔴src/input.rs:181andsrc/input.rs:184🔴src/input.rs:200🔴src/input.rs:221🔴(Note: As detailed in Critical Findings, this is unsound for non-ASCII
c. Subject to fixingeat_byteby addingc.is_ascii()validation):src/syntax_pos.rs:648🔴src/syntax_pos/hygiene.rs:151,183,204,247,382,415,461🔴(Note: For
is_descendant_ofline 204,least_ancestorline 247, andremove_markline 415, the CBOR buffer preallocation sizing vulnerability described in Critical Findings must be fixed):src/plugin/serialized.rs:103🔴(Note: As established in Critical Findings,
from_raw_ptrmust be promoted topub unsafe fn):src/errors/lock.rs:58,68,75🔴src/errors/diagnostic_builder.rs:178🟠src/syntax_pos/analyze_source_file.rs:67🟡