From 6ee9b8e71a88533b71d708d2ffc8e9e1ba05f5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= <199330534+cryptofixyup@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:23:05 +0200 Subject: [PATCH 1/5] Add sentinel-core crate --- crates/sentinel-core/Cargo.toml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 crates/sentinel-core/Cargo.toml diff --git a/crates/sentinel-core/Cargo.toml b/crates/sentinel-core/Cargo.toml new file mode 100644 index 0000000..d767a47 --- /dev/null +++ b/crates/sentinel-core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sentinel-core" +version = "0.1.0" +edition = "2021" +description = "Deterministic, no_std SentinelAI security enforcement core" +license = "Apache-2.0" + +[lib] +path = "src/lib.rs" + +[features] +default = [] + +[profile.release] +opt-level = 3 +lto = "fat" +codegen-units = 1 +panic = "abort" From 7a2d9aa3fd45c2b72609dc6d6f92f6b4c37e3c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= <199330534+cryptofixyup@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:23:08 +0200 Subject: [PATCH 2/5] Implement deterministic sentinel core --- crates/sentinel-core/src/lib.rs | 169 ++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 crates/sentinel-core/src/lib.rs diff --git a/crates/sentinel-core/src/lib.rs b/crates/sentinel-core/src/lib.rs new file mode 100644 index 0000000..2af4349 --- /dev/null +++ b/crates/sentinel-core/src/lib.rs @@ -0,0 +1,169 @@ +#![no_std] +#![forbid(unsafe_code)] + +use core::sync::atomic::{AtomicU8, Ordering}; + +pub const CACHE_LINE: usize = 64; +pub const MAX_EVENTS: usize = 32; +pub const MAX_INPUT: usize = 1024; +pub const ENTROPY_WINDOW: usize = 256; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum State { Monitor = 0, Anomalous = 1, Breached = 2 } + +impl State { + #[inline(always)] + pub const fn from_raw(v: u8) -> Self { match v { 0 => Self::Monitor, 1 => Self::Anomalous, _ => Self::Breached } } +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Decision { Allow = 0, Monitor = 1, Block = 2 } + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Signal { + None = 0, + ShellToken = 1, + ProcSelfCmdline = 2, + HighEntropy = 3, + LowEntropy = 4, + Tamper = 5, + Debugger = 6, + Root = 7, + CertificateMismatch = 8, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Event { pub signal: Signal, pub severity: u8, pub offset: u16 } +impl Event { pub const EMPTY: Self = Self { signal: Signal::None, severity: 0, offset: 0 }; } + +#[repr(align(64))] +pub struct EventRing { entries: [Event; MAX_EVENTS], head: usize, len: usize } + +impl EventRing { + pub const fn new() -> Self { Self { entries: [Event::EMPTY; MAX_EVENTS], head: 0, len: 0 } } + #[inline(always)] + pub fn push(&mut self, event: Event) { + self.entries[self.head] = event; + self.head = (self.head + 1) % MAX_EVENTS; + if self.len < MAX_EVENTS { self.len += 1; } + } + #[inline(always)] pub const fn len(&self) -> usize { self.len } + #[inline(always)] + pub fn get(&self, index: usize) -> Option { + if index >= self.len { return None; } + let start = if self.len == MAX_EVENTS { self.head } else { 0 }; + Some(self.entries[(start + index) % MAX_EVENTS]) + } +} + +#[repr(align(64))] +pub struct EntropyWindow { bytes: [u8; ENTROPY_WINDOW], len: usize } + +impl EntropyWindow { + pub const fn new() -> Self { Self { bytes: [0; ENTROPY_WINDOW], len: 0 } } + #[inline(always)] + pub fn load(&mut self, input: &[u8]) { + let n = core::cmp::min(input.len(), ENTROPY_WINDOW); + self.bytes[..n].copy_from_slice(&input[..n]); + self.len = n; + } + #[inline] + pub fn shannon_milli_bits(&self) -> u32 { + if self.len == 0 { return 0; } + let mut counts = [0u16; 256]; + let mut i = 0; + while i < self.len { counts[self.bytes[i] as usize] = counts[self.bytes[i] as usize].saturating_add(1); i += 1; } + let total = self.len as u32; + let mut sum = 0u32; + i = 0; + while i < 256 { + let c = counts[i] as u32; + if c != 0 { sum = sum.saturating_add(entropy_term_milli(c, total)); } + i += 1; + } + sum + } +} + +#[inline] +fn entropy_term_milli(count: u32, total: u32) -> u32 { + if count == 0 || total == 0 { return 0; } + let ratio_q12 = (count.saturating_mul(4096)) / total; + if ratio_q12 == 0 { return 0; } + let floor_log2 = integer_log2(ratio_q12); + let denom = 1u32 << floor_log2; + let frac = ratio_q12.saturating_sub(denom).saturating_mul(1000) / denom; + let log2_milli = floor_log2.saturating_mul(1000).saturating_add(frac); + (count.saturating_mul(log2_milli)) / total +} + +#[inline(always)] +fn integer_log2(mut v: u32) -> u32 { let mut n = 0; while v > 1 { v >>= 1; n += 1; } n } + +#[repr(align(64))] +pub struct SentinelCore { state: AtomicU8, ring: EventRing, entropy: EntropyWindow, score: u16 } + +impl SentinelCore { + pub const fn new() -> Self { Self { state: AtomicU8::new(State::Monitor as u8), ring: EventRing::new(), entropy: EntropyWindow::new(), score: 0 } } + #[inline(always)] pub fn state(&self) -> State { State::from_raw(self.state.load(Ordering::Acquire)) } + #[inline(always)] pub const fn score(&self) -> u16 { self.score } + #[inline(always)] fn raise_state(&self, next: State) { + let current = self.state.load(Ordering::Acquire); + if next as u8 > current { self.state.store(next as u8, Ordering::Release); } + } + #[inline(always)] + pub fn observe(&mut self, signal: Signal, offset: usize) { + let severity = match signal { Signal::None => 0, Signal::ShellToken => 30, Signal::ProcSelfCmdline => 60, Signal::HighEntropy => 20, Signal::LowEntropy => 10, Signal::Tamper => 90, Signal::Debugger => 70, Signal::Root => 80, Signal::CertificateMismatch => 60 }; + self.score = self.score.saturating_add(severity as u16); + self.ring.push(Event { signal, severity, offset: offset.min(u16::MAX as usize) as u16 }); + if self.score >= 100 { self.raise_state(State::Breached); } else if self.score >= 50 { self.raise_state(State::Anomalous); } + } + #[inline(always)] + pub fn scan(&mut self, input: &[u8]) -> Decision { + if self.state() == State::Breached { return Decision::Block; } + let n = core::cmp::min(input.len(), MAX_INPUT); + let data = &input[..n]; + self.entropy.load(data); + self.scan_token(data, b"/system/bin/sh", Signal::ShellToken); + self.scan_token(data, b"/system/bin/su", Signal::ShellToken); + self.scan_token(data, b"/bin/sh", Signal::ShellToken); + self.scan_token(data, b"busybox", Signal::ShellToken); + if let Some(offset) = find_subslice(data, b"/proc/self/cmdline") { self.observe(Signal::ProcSelfCmdline, offset); } + let entropy = self.entropy.shannon_milli_bits(); + if entropy > 7000 { self.observe(Signal::HighEntropy, 0); } else if n > 32 && entropy < 1500 { self.observe(Signal::LowEntropy, 0); } + self.decision() + } + #[inline(always)] fn scan_token(&mut self, data: &[u8], token: &[u8], signal: Signal) { if let Some(offset) = find_subslice(data, token) { self.observe(signal, offset); } } + #[inline(always)] pub const fn decision(&self) -> Decision { match State::from_raw(self.state.load(Ordering::Acquire)) { State::Monitor => Decision::Allow, State::Anomalous => Decision::Monitor, State::Breached => Decision::Block } } + #[inline(always)] pub const fn event_count(&self) -> usize { self.ring.len() } + #[inline(always)] pub fn event(&self, index: usize) -> Option { self.ring.get(index) } +} + +#[inline] +pub fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option { + if needle.is_empty() { return Some(0); } + if needle.len() > haystack.len() { return None; } + let last = haystack.len() - needle.len(); + let mut i = 0; + while i <= last { + let mut j = 0; + while j < needle.len() && haystack[i + j] == needle[j] { j += 1; } + if j == needle.len() { return Some(i); } + i += 1; + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] fn clean_input_allows() { let mut c = SentinelCore::new(); assert_eq!(c.scan(b"ordinary mobile telemetry"), Decision::Allow); } + #[test] fn proc_cmdline_detected() { let mut c = SentinelCore::new(); c.scan(b"x/proc/self/cmdline"); assert_eq!(c.event_count(), 1); assert_eq!(c.event(0).unwrap().signal, Signal::ProcSelfCmdline); } + #[test] fn high_risk_breaches() { let mut c = SentinelCore::new(); c.observe(Signal::Tamper, 0); c.observe(Signal::Root, 1); assert_eq!(c.state(), State::Breached); assert_eq!(c.decision(), Decision::Block); } + #[test] fn breach_is_sticky() { let mut c = SentinelCore::new(); c.observe(Signal::Tamper, 0); c.observe(Signal::Root, 0); assert_eq!(c.scan(b"clean"), Decision::Block); } + #[test] fn ring_is_bounded() { let mut c = SentinelCore::new(); for i in 0..(MAX_EVENTS + 10) { c.observe(Signal::None, i); } assert_eq!(c.event_count(), MAX_EVENTS); } +} From 6314ed5798fd6b169bd0b7340cdee8ecdedc9ecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= <199330534+cryptofixyup@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:23:12 +0200 Subject: [PATCH 3/5] Add sentinel-core regression tests --- crates/sentinel-core/tests/core.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 crates/sentinel-core/tests/core.rs diff --git a/crates/sentinel-core/tests/core.rs b/crates/sentinel-core/tests/core.rs new file mode 100644 index 0000000..7f6f7fe --- /dev/null +++ b/crates/sentinel-core/tests/core.rs @@ -0,0 +1,25 @@ +use sentinel_core::{Decision, Signal, SentinelCore, State, MAX_EVENTS}; + +#[test] +fn shell_token_is_recorded() { + let mut core = SentinelCore::new(); + core.scan(b"exec /system/bin/sh -c test"); + assert!(core.event_count() > 0); + assert_eq!(core.event(0).unwrap().signal, Signal::ShellToken); +} + +#[test] +fn multiple_signals_cross_breach_threshold() { + let mut core = SentinelCore::new(); + core.observe(Signal::Debugger, 10); + core.observe(Signal::Root, 20); + assert_eq!(core.state(), State::Breached); + assert_eq!(core.decision(), Decision::Block); +} + +#[test] +fn ring_does_not_grow_beyond_capacity() { + let mut core = SentinelCore::new(); + for i in 0..(MAX_EVENTS + 8) { core.observe(Signal::None, i); } + assert_eq!(core.event_count(), MAX_EVENTS); +} From 67d195de83f8ac536c6c4c364b9b250df5afd678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= <199330534+cryptofixyup@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:23:16 +0200 Subject: [PATCH 4/5] Document sentinel-core security boundary --- crates/sentinel-core/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 crates/sentinel-core/README.md diff --git a/crates/sentinel-core/README.md b/crates/sentinel-core/README.md new file mode 100644 index 0000000..695fd41 --- /dev/null +++ b/crates/sentinel-core/README.md @@ -0,0 +1,26 @@ +# sentinel-core + +Deterministic, allocation-free Rust security core for SentinelAI. + +## Security boundary + +- `#![no_std]` +- `#![forbid(unsafe_code)]` +- bounded input and event storage +- fixed-capacity ring buffer +- cache-line-aligned hot structures +- `AtomicU8` state machine with Acquire/Release ordering +- deterministic scoring +- bounded byte-token scanning +- fixed-window entropy analysis +- no networking or filesystem access + +The crate is enforcement-only. Telemetry, intelligence, ML, and policy distribution remain outside the trusted enforcement core. + +## Validation + +```text +cargo test --workspace +``` + +Build and test this crate on an Android AArch64 target as part of the release gate. From a617da84ffd950d3357d6aab0f5b7e467dbfc961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= <199330534+cryptofixyup@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:23:34 +0200 Subject: [PATCH 5/5] Wire sentinel-core into workspace --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 9f25810..a81a6fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,10 @@ name = "sentinel-wire" version = "0.1.0" edition = "2021" +[workspace] +members = ["crates/sentinel-core"] +resolver = "2" + [dependencies] crc32c = "0.6"