initial commit
Some checks failed
CI / lint (push) Successful in 1m37s
CI / test-python (push) Successful in 1m49s
CI / test-zig (push) Successful in 1m39s
CI / test-wasm (push) Successful in 1m54s
CI / test (push) Successful in 14m44s
CI / miri (push) Successful in 14m18s
CI / build (push) Successful in 1m9s
CI / fuzz-regression (push) Successful in 9m9s
CI / publish (push) Failing after 1m10s
CI / publish-python (push) Failing after 1m46s
CI / publish-wasm (push) Has been cancelled

Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
This commit is contained in:
Kamal Tufekcic 2026-04-02 23:48:10 +03:00
commit 1d99048c95
No known key found for this signature in database
165830 changed files with 79062 additions and 0 deletions

View file

@ -0,0 +1,21 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use soliton_capi::SolitonDecodedSessionInit;
use std::mem::MaybeUninit;
fuzz_target!(|data: &[u8]| {
// Exercise the CAPI session init decoder with adversarial wire bytes.
// Tests length validation, size cap (64 KiB), field parsing, UTF-8
// crypto_version allocation, and the free path.
let mut out = MaybeUninit::<SolitonDecodedSessionInit>::zeroed();
let rc = unsafe {
soliton_capi::soliton_kex_decode_session_init(
data.as_ptr(), data.len(), out.as_mut_ptr(),
)
};
if rc == 0 {
unsafe {
soliton_capi::soliton_decoded_session_init_free(out.as_mut_ptr());
}
}
});

View file

@ -0,0 +1,52 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use soliton_capi::{SolitonBuf, SolitonKeyRing};
use std::ffi::CString;
use std::ptr;
const FUZZ_KEY: [u8; 32] = [0x42; 32];
fuzz_target!(|data: &[u8]| {
// Exercise the CAPI DM queue decryption path with adversarial input.
// Covers attack surface beyond the core fuzz target: check_len! on
// recipient_fp_len, cstr_to_str batch_id parsing (interior NUL),
// null-pointer guards, MAX_BLOB_LEN cap, keyring reentrancy guard.
let mut keyring: *mut SolitonKeyRing = ptr::null_mut();
let rc = unsafe {
soliton_capi::soliton_keyring_new(FUZZ_KEY.as_ptr(), 32, 1, &mut keyring)
};
if rc != 0 || keyring.is_null() {
return;
}
// Split fuzz input: first byte selects recipient_fp_len to exercise
// check_len! with invalid sizes, remaining bytes are the blob.
if data.is_empty() {
unsafe { soliton_capi::soliton_keyring_free(&mut keyring) };
return;
}
let fp_len = data[0] as usize;
let blob = &data[1..];
// Fixed recipient fingerprint — the fp_len variation tests the
// check_len! guard path, not fingerprint content.
let recipient_fp = [0xAAu8; 32];
let batch_id = CString::new("fuzz-batch").unwrap();
let mut plaintext_out = SolitonBuf { ptr: ptr::null_mut(), len: 0 };
let _ = unsafe {
soliton_capi::soliton_dm_queue_decrypt(
keyring,
blob.as_ptr(), blob.len(),
recipient_fp.as_ptr(), fp_len,
batch_id.as_ptr(),
&mut plaintext_out,
)
};
if !plaintext_out.ptr.is_null() {
unsafe { soliton_capi::soliton_buf_free(&mut plaintext_out) };
}
unsafe { soliton_capi::soliton_keyring_free(&mut keyring) };
});

View file

@ -0,0 +1,17 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use soliton_capi::SolitonRatchet;
use std::ptr;
fuzz_target!(|data: &[u8]| {
// Exercise the CAPI ratchet deserialization path with adversarial input.
// Tests null-pointer guards, length validation, magic number setup,
// CAS guard handling, and proper cleanup on both success and failure paths.
let mut ratchet: *mut SolitonRatchet = ptr::null_mut();
let rc = unsafe {
soliton_capi::soliton_ratchet_from_bytes(data.as_ptr(), data.len(), &mut ratchet)
};
if rc == 0 && !ratchet.is_null() {
unsafe { soliton_capi::soliton_ratchet_free(&mut ratchet); }
}
});

View file

@ -0,0 +1,41 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use soliton_capi::{SolitonBuf, SolitonKeyRing};
use std::ffi::CString;
use std::ptr;
const FUZZ_KEY: [u8; 32] = [0x42; 32];
fuzz_target!(|data: &[u8]| {
// Exercise the CAPI storage decryption path with adversarial blob input.
// A valid keyring is constructed once; the fuzz input is the encrypted blob.
// Tests version routing, flag validation, AEAD tag check, decompression
// bounds, and SolitonBuf output allocation.
let mut keyring: *mut SolitonKeyRing = ptr::null_mut();
let rc = unsafe {
soliton_capi::soliton_keyring_new(FUZZ_KEY.as_ptr(), 32, 1, &mut keyring)
};
if rc != 0 || keyring.is_null() {
return;
}
let channel = CString::new("fuzz-ch").unwrap();
let segment = CString::new("fuzz-seg").unwrap();
let mut plaintext_out = SolitonBuf { ptr: ptr::null_mut(), len: 0 };
let _ = unsafe {
soliton_capi::soliton_storage_decrypt(
keyring,
data.as_ptr(), data.len(),
channel.as_ptr(),
segment.as_ptr(),
&mut plaintext_out,
)
};
if !plaintext_out.ptr.is_null() {
unsafe { soliton_capi::soliton_buf_free(&mut plaintext_out); }
}
unsafe { soliton_capi::soliton_keyring_free(&mut keyring); }
});

View file

@ -0,0 +1,53 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use std::ptr;
fuzz_target!(|data: &[u8]| {
// Exercise the CAPI streaming decryption path with adversarial input.
// Tests header validation, AEAD rejection, decompression bounds,
// and output buffer handling through the FFI boundary.
if data.len() < 26 {
return;
}
let key = [0x42u8; 32];
let header = &data[..26];
let rest = &data[26..];
let mut dec: *mut soliton_capi::SolitonStreamDecryptor = ptr::null_mut();
let rc = unsafe {
soliton_capi::soliton_stream_decrypt_init(
key.as_ptr(), 32,
header.as_ptr(), 26,
ptr::null(), 0,
&mut dec,
)
};
if rc != 0 || dec.is_null() {
return;
}
// Feed remaining bytes as chunks in 2048-byte slices.
// 1 MiB chunk size + 256-byte zstd overhead margin — matches STREAM_CHUNK_SIZE.
let out_cap = 1_048_576 + 256;
let mut out_buf = vec![0u8; out_cap];
for chunk_data in rest.chunks(2048) {
let mut out_written: usize = 0;
let mut is_final: bool = false;
let rc = unsafe {
soliton_capi::soliton_stream_decrypt_chunk(
dec,
chunk_data.as_ptr(), chunk_data.len(),
out_buf.as_mut_ptr(), out_cap,
&mut out_written,
&mut is_final,
)
};
if rc != 0 || is_final {
break;
}
}
unsafe { soliton_capi::soliton_stream_decrypt_free(&mut dec); }
});

View file

@ -0,0 +1,56 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use std::ptr;
// Exercise the CAPI random-access streaming decryption path with adversarial
// input. Unlike the sequential fuzz_capi_stream_decrypt target, this tests
// index-based nonce derivation — the fuzzer controls both the chunk data and
// the chunk index, exploring index arithmetic edge cases (overflow, large
// gaps, u64::MAX boundary) that sequential processing never reaches.
fuzz_target!(|data: &[u8]| {
// Minimum: 26 (header) + 8 (index) + 1 (chunk data).
if data.len() < 35 {
return;
}
let key = [0x42u8; 32];
let header = &data[..26];
let index_bytes = &data[26..34];
let chunk_data = &data[34..];
// Fuzz-controlled index — exercises nonce derivation for arbitrary positions.
let index = u64::from_le_bytes(index_bytes.try_into().unwrap());
let mut dec: *mut soliton_capi::SolitonStreamDecryptor = ptr::null_mut();
let rc = unsafe {
soliton_capi::soliton_stream_decrypt_init(
key.as_ptr(), 32,
header.as_ptr(), 26,
ptr::null(), 0,
&mut dec,
)
};
if rc != 0 || dec.is_null() {
return;
}
// 1 MiB + 256-byte zstd overhead margin.
let out_cap = 1_048_576 + 256;
let mut out_buf = vec![0u8; out_cap];
let mut out_written: usize = 0;
let mut is_final: bool = false;
// Single chunk at a fuzz-controlled index.
let _ = unsafe {
soliton_capi::soliton_stream_decrypt_chunk_at(
dec,
index,
chunk_data.as_ptr(), chunk_data.len(),
out_buf.as_mut_ptr(), out_cap,
&mut out_written,
&mut is_final,
)
};
unsafe { soliton_capi::soliton_stream_decrypt_free(&mut dec); }
});

View file

@ -0,0 +1,66 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use std::ptr;
// Exercise the CAPI random-access streaming encryption path with adversarial
// input. Tests index-based nonce derivation on the encryption side — the
// fuzzer controls the chunk index and plaintext, exploring nonce arithmetic
// edge cases and output buffer sizing for non-standard chunk positions.
//
// Non-final chunks must be exactly STREAM_CHUNK_SIZE (1 MiB). With libFuzzer's
// default max_len of 4096, plaintext is always shorter — so is_last is forced
// true for short inputs. The fuzz byte only controls is_last when plaintext
// happens to be exactly 1 MiB (requires -max_len=1048585 to reach).
fuzz_target!(|data: &[u8]| {
// Minimum: 8 (index) + 1 (is_last flag) + 0 (plaintext can be empty).
if data.len() < 9 {
return;
}
let key = [0x42u8; 32];
let index = u64::from_le_bytes(data[..8].try_into().unwrap());
let plaintext = &data[9..];
// Non-final chunks require exactly 1,048,576 bytes of plaintext.
// Force is_last=true for any other size so the fuzzer can explore
// the success path with short inputs. The fuzz byte only takes
// effect when plaintext happens to be exactly STREAM_CHUNK_SIZE.
const STREAM_CHUNK_SIZE: usize = 1_048_576;
let is_last = if plaintext.len() == STREAM_CHUNK_SIZE {
data[8] & 1 != 0
} else {
true
};
let mut enc: *mut soliton_capi::SolitonStreamEncryptor = ptr::null_mut();
let rc = unsafe {
soliton_capi::soliton_stream_encrypt_init(
key.as_ptr(), 32,
ptr::null(), 0,
false, // compress
&mut enc,
)
};
if rc != 0 || enc.is_null() {
return;
}
// SOLITON_STREAM_ENCRYPT_MAX = 1_048_849.
let out_cap = 1_048_849;
let mut out_buf = vec![0u8; out_cap];
let mut out_written: usize = 0;
let _ = unsafe {
soliton_capi::soliton_stream_encrypt_chunk_at(
enc,
index,
plaintext.as_ptr(), plaintext.len(),
is_last,
out_buf.as_mut_ptr(), out_cap,
&mut out_written,
)
};
unsafe { soliton_capi::soliton_stream_encrypt_free(&mut enc); }
});