security: harden regex native ffi
ober
5271f35e9a49586f75fad3bfbe7350ec8f6ae158
--- a/docs/ffi-audit.md +++ b/docs/ffi-audit.md @@ -40,10 +40,10 @@ Latest summary from `tools/ffi-audit-report.ss`: (rust-file-count 29) (c-file-count 5) (no-mangle-exports 196) - (unsafe-sites 439) - (unsafe-sites-with-nearby-safety-comment 143) - (unsafe-sites-without-nearby-safety-comment 296) - (unsafe-comment-review-sites 296) + (unsafe-sites 432) + (unsafe-sites-with-nearby-safety-comment 151) + (unsafe-sites-without-nearby-safety-comment 281) + (unsafe-comment-review-sites 281) (export-review-sites 196)) (vendor (jsqlite (path "vendor/jsqlite") (status accepted-risk-cve-gated))) @@ -95,8 +95,12 @@ rejects null nonempty plaintext and output-length overflow before creating FFI slices. `integrity.rs` now documents self-hash, signature-verification, file region hashing, and FreeBSD `sysctl` unsafe invariants, checks exclusion-region arithmetic before slicing, and streams caller-sized file regions instead of -allocating a caller-controlled `u64` length. The generated inventory now reports -143 annotated native unsafe sites and 296 remaining unsafe review sites. +allocating a caller-controlled `u64` length. `regex_native.rs` now validates +nullable byte buffers through a shared helper before creating Rust slices, +documents output-slot and ovector invariants, checks capture slot arithmetic, +and returns a normal FFI error if the regex handle store is poisoned. The +generated inventory now reports 151 annotated native unsafe sites and 281 +remaining unsafe review sites. Remaining work before closing K3-P1-01: --- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -639,10 +639,13 @@ not started." `integrity.rs` now documents self-hash, signature-verification, file-region hashing, and FreeBSD `sysctl` unsafe invariants, checks exclusion-region arithmetic before slicing, and streams caller-sized file regions instead of - allocating a caller-controlled `u64` length. The generated report now shows - 143 annotated native unsafe sites and 296 remaining unsafe review sites. - Remaining work: continue unsafe invariant comments across the rest of - `jerboa-native-rs`. + allocating a caller-controlled `u64` length. `regex_native.rs` now validates + nullable byte buffers through a shared helper before creating Rust slices, + documents output-slot and ovector invariants, checks capture slot arithmetic, + and returns a normal FFI error if the regex handle store is poisoned. The + generated report now shows 151 annotated native unsafe sites and 281 remaining + unsafe review sites. Remaining work: continue unsafe invariant comments across + the rest of `jerboa-native-rs`. ### K3-P1-02 — TOCTOU-safe filesystem capability checks **Serves:** G2. **Effort:** 1 week. @@ -1130,7 +1133,7 @@ Track these in `docs/status.md` per release: | Confined worker exists | facade, audit lifecycle, output caps, deadline, process-group kill, memory rlimit pre-exec path, explicit sandbox-axis refusal, egress proxy env wiring, and Linux syscall/ptrace seccomp pre-exec setup landed; Landlock path/net and Seatbelt/Capsicum parity pending | yes, tested (P0-02) | | Unclassified raw `read`/FASL/`load` sites | scanner-driven manifest classification gate closed 2026-07-27; trusted build, compiler, REPL/dev, and test surfaces are centrally justified by exact source line | 0 | | FFI bindings audited | scanner inventory and provisional verdicts in [ffi-audit.md](ffi-audit.md); native exports gated and justified | 100% with final verdicts | -| Un-annotated Rust `unsafe` blocks | 296 unsafe review sites without nearby `SAFETY:` comments in [ffi-audit.md](ffi-audit.md) | 0 | +| Un-annotated Rust `unsafe` blocks | 281 unsafe review sites without nearby `SAFETY:` comments in [ffi-audit.md](ffi-audit.md) | 0 | | Fuzz corpora / crash regressions | 0 / 0 | per-parser corpora + every crash a test | | Exploit-shaped regression tests | ~0 | ≥ 1 per historical finding | | Sandboxed-by-default app entry | no | yes (P0-06) | --- a/docs/reviews/2026-07-27-native-export-review.sexp +++ b/docs/reviews/2026-07-27-native-export-review.sexp @@ -100,14 +100,14 @@ (export (symbol "jerboa_pg_nrows") (file "jerboa-native-rs/src/postgres_native.rs") (line 215) (scheme-callers 1 (caller (file "lib/std/db/postgresql-native.ss") (line 32)))) (export (symbol "jerboa_pg_query") (file "jerboa-native-rs/src/postgres_native.rs") (line 172) (scheme-callers 1 (caller (file "lib/std/db/postgresql-native.ss") (line 30)))) (export (symbol "jerboa_random_bytes") (file "jerboa-native-rs/src/crypto.rs") (line 285) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 98)))) - (export (symbol "jerboa_regex_captures") (file "jerboa-native-rs/src/regex_native.rs") (line 274) (scheme-callers 2 (caller (file "lib/std/regex-native.ss") (line 40)) (caller (file "lib/std/regex.ss") (line 106)))) - (export (symbol "jerboa_regex_compile") (file "jerboa-native-rs/src/regex_native.rs") (line 14) (scheme-callers 3 (caller (file "lib/std/regex-native.ss") (line 27)) (caller (file "lib/std/regex.ss") (line 81)) (caller (file "lib/std/regex.ss") (line 85)))) - (export (symbol "jerboa_regex_find") (file "jerboa-native-rs/src/regex_native.rs") (line 75) (scheme-callers 4 (caller (file "lib/std/regex-native.ss") (line 33)) (caller (file "lib/std/regex.ss") (line 89)) (caller (file "lib/std/regex.ss") (line 97)) (caller (file "lib/std/regex.ss") (line 657)))) - (export (symbol "jerboa_regex_find_at") (file "jerboa-native-rs/src/regex_native.rs") (line 228) (scheme-callers 3 (caller (file "lib/std/regex-native.ss") (line 33)) (caller (file "lib/std/regex.ss") (line 97)) (caller (file "lib/std/regex.ss") (line 657)))) - (export (symbol "jerboa_regex_free") (file "jerboa-native-rs/src/regex_native.rs") (line 157) (scheme-callers 2 (caller (file "lib/std/regex-native.ss") (line 48)) (caller (file "lib/std/regex.ss") (line 93)))) - (export (symbol "jerboa_regex_group_count") (file "jerboa-native-rs/src/regex_native.rs") (line 327) (scheme-callers 2 (caller (file "lib/std/regex-native.ss") (line 37)) (caller (file "lib/std/regex.ss") (line 102)))) - (export (symbol "jerboa_regex_is_match") (file "jerboa-native-rs/src/regex_native.rs") (line 49) (scheme-callers 1 (caller (file "lib/std/regex-native.ss") (line 30)))) - (export (symbol "jerboa_regex_replace_all") (file "jerboa-native-rs/src/regex_native.rs") (line 112) (scheme-callers 1 (caller (file "lib/std/regex-native.ss") (line 44)))) + (export (symbol "jerboa_regex_captures") (file "jerboa-native-rs/src/regex_native.rs") (line 357) (scheme-callers 2 (caller (file "lib/std/regex-native.ss") (line 40)) (caller (file "lib/std/regex.ss") (line 106)))) + (export (symbol "jerboa_regex_compile") (file "jerboa-native-rs/src/regex_native.rs") (line 38) (scheme-callers 3 (caller (file "lib/std/regex-native.ss") (line 27)) (caller (file "lib/std/regex.ss") (line 81)) (caller (file "lib/std/regex.ss") (line 85)))) + (export (symbol "jerboa_regex_find") (file "jerboa-native-rs/src/regex_native.rs") (line 114) (scheme-callers 4 (caller (file "lib/std/regex-native.ss") (line 33)) (caller (file "lib/std/regex.ss") (line 89)) (caller (file "lib/std/regex.ss") (line 97)) (caller (file "lib/std/regex.ss") (line 657)))) + (export (symbol "jerboa_regex_find_at") (file "jerboa-native-rs/src/regex_native.rs") (line 303) (scheme-callers 3 (caller (file "lib/std/regex-native.ss") (line 33)) (caller (file "lib/std/regex.ss") (line 97)) (caller (file "lib/std/regex.ss") (line 657)))) + (export (symbol "jerboa_regex_free") (file "jerboa-native-rs/src/regex_native.rs") (line 219) (scheme-callers 2 (caller (file "lib/std/regex-native.ss") (line 48)) (caller (file "lib/std/regex.ss") (line 93)))) + (export (symbol "jerboa_regex_group_count") (file "jerboa-native-rs/src/regex_native.rs") (line 431) (scheme-callers 2 (caller (file "lib/std/regex-native.ss") (line 37)) (caller (file "lib/std/regex.ss") (line 102)))) + (export (symbol "jerboa_regex_is_match") (file "jerboa-native-rs/src/regex_native.rs") (line 82) (scheme-callers 1 (caller (file "lib/std/regex-native.ss") (line 30)))) + (export (symbol "jerboa_regex_replace_all") (file "jerboa-native-rs/src/regex_native.rs") (line 159) (scheme-callers 1 (caller (file "lib/std/regex-native.ss") (line 44)))) (export (symbol "jerboa_scrypt") (file "jerboa-native-rs/src/crypto.rs") (line 694) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 226)))) (export (symbol "jerboa_seccomp_available") (file "jerboa-native-rs/src/seccomp.rs") (line 285) (scheme-callers 1 (caller (file "lib/std/os/seccomp.ss") (line 32)))) (export (symbol "jerboa_seccomp_lock") (file "jerboa-native-rs/src/seccomp.rs") (line 179) (scheme-callers 2 (caller (file "lib/std/os/seccomp.ss") (line 34)) (caller (file "lib/std/os/seccomp.ss") (line 36)))) --- a/docs/status.md +++ b/docs/status.md @@ -25,7 +25,7 @@ release artifacts are built as Jerboa multicall binaries with `jerboa`, | Area | Current state | Remaining work | |---|---|---| | Kimi security handoff | [kimi3-security-recommmendations.md](kimi3-security-recommmendations.md) is the backlog. Dated evidence and review manifests live under [reviews/](reviews/). | Keep new security evidence in dated review records and summarize the current release state here. | -| FFI audit phase 5 | [ffi-audit.md](ffi-audit.md) records the scanner output, provisional Scheme binding verdicts, and native Rust export inventory. `make native-export-review-check` gates native export decisions. The worker-launch native path in `aproc.rs`/`seccomp.rs`, crypto FFI buffer path in `crypto.rs`, secure-memory region lifecycle in `secure_mem.rs`, Ed25519/X25519 key-agreement buffers, compression buffers, HTTP parse/writev boundary, embed-crypto ABI, and integrity ABI now have nearby `SAFETY:` comments, with the generated unannotated unsafe-site count at 296. | Continue adding `SAFETY:` invariant comments near the remaining Rust unsafe sites. | +| FFI audit phase 5 | [ffi-audit.md](ffi-audit.md) records the scanner output, provisional Scheme binding verdicts, and native Rust export inventory. `make native-export-review-check` gates native export decisions. The worker-launch native path in `aproc.rs`/`seccomp.rs`, crypto FFI buffer path in `crypto.rs`, secure-memory region lifecycle in `secure_mem.rs`, Ed25519/X25519 key-agreement buffers, compression buffers, HTTP parse/writev boundary, embed-crypto ABI, integrity ABI, and regex-native ABI now have nearby `SAFETY:` comments, with the generated unannotated unsafe-site count at 281. | Continue adding `SAFETY:` invariant comments near the remaining Rust unsafe sites. | | Native Rust exports | The native export review now has 190 exported functions: 183 tracked Scheme references and 7 retained standalone C/binary helpers. The previous 35 no-Scheme-reference removal candidates no longer have C ABI export markers. | Re-run `make native-export-review-check` whenever adding or removing native exports. | | Confined worker | `(std security worker)` provides the facade, audit lifecycle, output caps, deadlines, process-group kill, memory rlimit pre-exec setup, Linux syscall/ptrace seccomp pre-exec setup for requested axes, explicit sandbox-axis refusal, and egress proxy env wiring. | Install native Landlock path/net rules in the worker pre-exec path and keep Linux/macOS/FreeBSD parity tests current. | | Safe surface | Direct scripts default to the safe prelude; raw access requires `--unsafe-prelude` or `(jerboa prelude unsafe)`. | Continue moving risky APIs behind explicit unsafe imports as new modules land. | --- a/jerboa-native-rs/src/regex_native.rs +++ b/jerboa-native-rs/src/regex_native.rs @@ -4,12 +4,36 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::LazyLock; use std::sync::Mutex; +use std::sync::MutexGuard; // Opaque handle system static REGEX_STORE: LazyLock<Mutex<HashMap<u64, Regex>>> = LazyLock::new(|| Mutex::new(HashMap::new())); static NEXT_ID: AtomicU64 = AtomicU64::new(1); +fn lock_regex_store() -> Result<MutexGuard<'static, HashMap<u64, Regex>>, i32> { + match REGEX_STORE.lock() { + Ok(guard) => Ok(guard), + Err(_) => { + set_last_error("regex store lock poisoned".to_string()); + Err(-1) + } + } +} + +fn ffi_bytes<'a>(ptr: *const u8, len: usize, name: &str) -> Result<&'a [u8], i32> { + if ptr.is_null() { + if len == 0 { + return Ok(&[]); + } + set_last_error(format!("null {} pointer", name)); + return Err(-1); + } + // SAFETY: ptr is non-null above and the C ABI contract requires a readable + // len-byte buffer for this input parameter. + Ok(unsafe { std::slice::from_raw_parts(ptr, len) }) +} + #[no_mangle] pub extern "C" fn jerboa_regex_compile( pattern: *const u8, @@ -17,10 +41,13 @@ pub extern "C" fn jerboa_regex_compile( handle: *mut u64, ) -> i32 { ffi_wrap(|| { - if pattern.is_null() || handle.is_null() { + if handle.is_null() { return -1; } - let pat_bytes = unsafe { std::slice::from_raw_parts(pattern, pattern_len) }; + let pat_bytes = match ffi_bytes(pattern, pattern_len, "pattern") { + Ok(bytes) => bytes, + Err(code) => return code, + }; let pat = match std::str::from_utf8(pat_bytes) { Ok(s) => s, Err(_) => { @@ -31,7 +58,13 @@ pub extern "C" fn jerboa_regex_compile( match Regex::new(pat) { Ok(re) => { let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); - REGEX_STORE.lock().unwrap().insert(id, re); + let mut store = match lock_regex_store() { + Ok(store) => store, + Err(code) => return code, + }; + store.insert(id, re); + // SAFETY: handle is non-null above and points to a writable + // u64 result slot owned by the caller. unsafe { *handle = id; } @@ -48,7 +81,10 @@ pub extern "C" fn jerboa_regex_compile( #[no_mangle] pub extern "C" fn jerboa_regex_is_match(handle: u64, text: *const u8, text_len: usize) -> i32 { ffi_wrap(|| { - let store = REGEX_STORE.lock().unwrap(); + let store = match lock_regex_store() { + Ok(store) => store, + Err(code) => return code, + }; let re = match store.get(&handle) { Some(r) => r, None => { @@ -56,7 +92,10 @@ pub extern "C" fn jerboa_regex_is_match(handle: u64, text: *const u8, text_len: return -1; } }; - let text_bytes = unsafe { std::slice::from_raw_parts(text, text_len) }; + let text_bytes = match ffi_bytes(text, text_len, "text") { + Ok(bytes) => bytes, + Err(code) => return code, + }; let s = match std::str::from_utf8(text_bytes) { Ok(s) => s, Err(_) => return -1, @@ -83,18 +122,26 @@ pub extern "C" fn jerboa_regex_find( if match_start.is_null() || match_end.is_null() { return -1; } - let store = REGEX_STORE.lock().unwrap(); + let store = match lock_regex_store() { + Ok(store) => store, + Err(code) => return code, + }; let re = match store.get(&handle) { Some(r) => r, None => return -1, }; - let text_bytes = unsafe { std::slice::from_raw_parts(text, text_len) }; + let text_bytes = match ffi_bytes(text, text_len, "text") { + Ok(bytes) => bytes, + Err(code) => return code, + }; let s = match std::str::from_utf8(text_bytes) { Ok(s) => s, Err(_) => return -1, }; match re.find(s) { Some(m) => { + // SAFETY: match_start and match_end are non-null above and + // point to writable usize result slots. unsafe { *match_start = m.start(); *match_end = m.end(); @@ -123,17 +170,26 @@ pub extern "C" fn jerboa_regex_replace_all( if output.is_null() || output_len.is_null() { return -1; } - let store = REGEX_STORE.lock().unwrap(); + let store = match lock_regex_store() { + Ok(store) => store, + Err(code) => return code, + }; let re = match store.get(&handle) { Some(r) => r, None => return -1, }; - let text_bytes = unsafe { std::slice::from_raw_parts(text, text_len) }; + let text_bytes = match ffi_bytes(text, text_len, "text") { + Ok(bytes) => bytes, + Err(code) => return code, + }; let s = match std::str::from_utf8(text_bytes) { Ok(s) => s, Err(_) => return -1, }; - let repl_bytes = unsafe { std::slice::from_raw_parts(replacement, repl_len) }; + let repl_bytes = match ffi_bytes(replacement, repl_len, "replacement") { + Ok(bytes) => bytes, + Err(code) => return code, + }; let r = match std::str::from_utf8(repl_bytes) { Ok(s) => s, Err(_) => return -1, @@ -144,8 +200,14 @@ pub extern "C" fn jerboa_regex_replace_all( set_last_error("output buffer too small".to_string()); return -1; } - let out = unsafe { std::slice::from_raw_parts_mut(output, output_max) }; - out[..result_bytes.len()].copy_from_slice(result_bytes); + if !result_bytes.is_empty() { + // SAFETY: output is non-null above, result length is <= output_max, + // and callers provide writable capacity for the result bytes. + let out = unsafe { std::slice::from_raw_parts_mut(output, result_bytes.len()) }; + out.copy_from_slice(result_bytes); + } + // SAFETY: output_len is non-null above and points to a writable usize + // result slot. unsafe { *output_len = result_bytes.len(); } @@ -156,7 +218,11 @@ pub extern "C" fn jerboa_regex_replace_all( #[no_mangle] pub extern "C" fn jerboa_regex_free(handle: u64) -> i32 { ffi_wrap(|| { - REGEX_STORE.lock().unwrap().remove(&handle); + let mut store = match lock_regex_store() { + Ok(store) => store, + Err(code) => return code, + }; + store.remove(&handle); 0 }) } @@ -173,10 +239,13 @@ pub extern "C" fn jerboa_regex_compile_ex( handle: *mut u64, ) -> i32 { ffi_wrap(|| { - if pattern.is_null() || handle.is_null() { + if handle.is_null() { return -1; } - let pat_bytes = unsafe { std::slice::from_raw_parts(pattern, pattern_len) }; + let pat_bytes = match ffi_bytes(pattern, pattern_len, "pattern") { + Ok(bytes) => bytes, + Err(code) => return code, + }; let pat = match std::str::from_utf8(pat_bytes) { Ok(s) => s, Err(_) => { @@ -208,7 +277,13 @@ pub extern "C" fn jerboa_regex_compile_ex( match Regex::new(&full_pattern) { Ok(re) => { let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); - REGEX_STORE.lock().unwrap().insert(id, re); + let mut store = match lock_regex_store() { + Ok(store) => store, + Err(code) => return code, + }; + store.insert(id, re); + // SAFETY: handle is non-null above and points to a writable + // u64 result slot owned by the caller. unsafe { *handle = id; } @@ -237,12 +312,18 @@ pub extern "C" fn jerboa_regex_find_at( if match_start.is_null() || match_end.is_null() { return -1; } - let store = REGEX_STORE.lock().unwrap(); + let store = match lock_regex_store() { + Ok(store) => store, + Err(code) => return code, + }; let re = match store.get(&handle) { Some(r) => r, None => return -1, }; - let text_bytes = unsafe { std::slice::from_raw_parts(text, text_len) }; + let text_bytes = match ffi_bytes(text, text_len, "text") { + Ok(bytes) => bytes, + Err(code) => return code, + }; let s = match std::str::from_utf8(text_bytes) { Ok(s) => s, Err(_) => return -1, @@ -252,6 +333,8 @@ pub extern "C" fn jerboa_regex_find_at( } match re.find_at(s, start_offset) { Some(m) => { + // SAFETY: match_start and match_end are non-null above and + // point to writable usize result slots. unsafe { *match_start = m.start(); *match_end = m.end(); @@ -283,12 +366,18 @@ pub extern "C" fn jerboa_regex_captures( if ovector_buf.is_null() || ovector_capacity < 2 { return -1; } - let store = REGEX_STORE.lock().unwrap(); + let store = match lock_regex_store() { + Ok(store) => store, + Err(code) => return code, + }; let re = match store.get(&handle) { Some(r) => r, None => return -1, }; - let text_bytes = unsafe { std::slice::from_raw_parts(text, text_len) }; + let text_bytes = match ffi_bytes(text, text_len, "text") { + Ok(bytes) => bytes, + Err(code) => return code, + }; let s = match std::str::from_utf8(text_bytes) { Ok(s) => s, Err(_) => return -1, @@ -301,9 +390,24 @@ pub extern "C" fn jerboa_regex_captures( None => return 0, }; let num_groups = caps.len(); // includes group 0 (full match) - let slots_needed = num_groups * 2; + let slots_needed = match num_groups.checked_mul(2) { + Some(n) => n, + None => { + set_last_error("capture slot count overflow".to_string()); + return -1; + } + }; let slots_to_write = slots_needed.min(ovector_capacity); let groups_to_write = slots_to_write / 2; + let groups_written_i32 = match i32::try_from(groups_to_write) { + Ok(n) => n, + Err(_) => { + set_last_error("capture group count exceeds i32".to_string()); + return -1; + } + }; + // SAFETY: ovector_buf is non-null above, ovector_capacity is at least + // slots_to_write, and callers provide writable usize slots. let ov = unsafe { std::slice::from_raw_parts_mut(ovector_buf, slots_to_write) }; for i in 0..groups_to_write { match caps.get(i) { @@ -317,7 +421,7 @@ pub extern "C" fn jerboa_regex_captures( } } } - groups_to_write as i32 + groups_written_i32 }) } @@ -326,7 +430,10 @@ pub extern "C" fn jerboa_regex_captures( #[no_mangle] pub extern "C" fn jerboa_regex_group_count(handle: u64) -> i32 { ffi_wrap(|| { - let store = REGEX_STORE.lock().unwrap(); + let store = match lock_regex_store() { + Ok(store) => store, + Err(code) => return code, + }; let re = match store.get(&handle) { Some(r) => r, None => return -1, @@ -335,3 +442,64 @@ pub extern "C" fn jerboa_regex_group_count(handle: u64) -> i32 { (re.captures_len()) as i32 }) } + +#[cfg(test)] +mod tests { + use super::{ + jerboa_regex_compile, jerboa_regex_find, jerboa_regex_free, jerboa_regex_is_match, + jerboa_regex_replace_all, + }; + + fn compile(pattern: &str) -> u64 { + let mut handle = 0u64; + assert_eq!( + jerboa_regex_compile(pattern.as_ptr(), pattern.len(), &mut handle), + 0 + ); + handle + } + + #[test] + fn is_match_rejects_null_text_with_nonzero_length() { + let handle = compile("a+"); + + assert_eq!(jerboa_regex_is_match(handle, std::ptr::null(), 1), -1); + assert_eq!(jerboa_regex_free(handle), 0); + } + + #[test] + fn find_rejects_null_text_with_nonzero_length() { + let handle = compile("a+"); + let mut start = 0usize; + let mut end = 0usize; + + assert_eq!( + jerboa_regex_find(handle, std::ptr::null(), 1, &mut start, &mut end), + -1 + ); + assert_eq!(jerboa_regex_free(handle), 0); + } + + #[test] + fn replace_rejects_null_replacement_with_nonzero_length() { + let handle = compile("a+"); + let text = b"aaa"; + let mut output = [0u8; 8]; + let mut output_len = 0usize; + + assert_eq!( + jerboa_regex_replace_all( + handle, + text.as_ptr(), + text.len(), + std::ptr::null(), + 1, + output.as_mut_ptr(), + output.len(), + &mut output_len, + ), + -1 + ); + assert_eq!(jerboa_regex_free(handle), 0); + } +}