From 98f7e0f08bee9a2d25f15ca557a8eaaae1e0b0a7 Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Mon, 9 Mar 2026 15:41:46 +0100 Subject: [PATCH] feat(native-certgen): implement logging subsystem Multi-output logging via tracing: /dev/kmsg for logcat, rotating file appender (512KB, 3 files), stderr for debug. Diagnostic ZIP dump with log files and TEE status snapshots. Verbose toggle via JNI flag or .verbose marker file. --- native-certgen/src/lib.rs | 1 + native-certgen/src/logging/dump.rs | 208 +++++++++++++++++++++++++ native-certgen/src/logging/kmsg.rs | 93 +++++++++++ native-certgen/src/logging/mod.rs | 40 +++++ native-certgen/src/logging/rotating.rs | 167 ++++++++++++++++++++ native-certgen/src/logging/sysfs.rs | 38 +++++ 6 files changed, 547 insertions(+) create mode 100644 native-certgen/src/logging/dump.rs create mode 100644 native-certgen/src/logging/kmsg.rs create mode 100644 native-certgen/src/logging/mod.rs create mode 100644 native-certgen/src/logging/rotating.rs create mode 100644 native-certgen/src/logging/sysfs.rs diff --git a/native-certgen/src/lib.rs b/native-certgen/src/lib.rs index 4335d6c..9bbc550 100644 --- a/native-certgen/src/lib.rs +++ b/native-certgen/src/lib.rs @@ -3,3 +3,4 @@ mod types; mod keygen; pub mod keybox; pub mod attestation; +pub mod logging; diff --git a/native-certgen/src/logging/dump.rs b/native-certgen/src/logging/dump.rs new file mode 100644 index 0000000..95cc621 --- /dev/null +++ b/native-certgen/src/logging/dump.rs @@ -0,0 +1,208 @@ +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::path::Path; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +const DUMP_DIR: &str = "/sdcard/Download"; +const LOCK_PATH: &str = "/data/adb/tricky_store/.dump_lock"; +const DUMP_PATH_FILE: &str = "/data/adb/tricky_store/.dump_path"; +const LOG_DIR: &str = "/data/adb/tricky_store/logs"; +const BASE_DIR: &str = "/data/adb/tricky_store"; +const LOGCAT_SIZE_LIMIT: usize = 2 * 1024 * 1024; + +struct FlockGuard { + _file: File, +} + +impl FlockGuard { + fn acquire() -> Result> { + if let Some(parent) = Path::new(LOCK_PATH).parent() { + fs::create_dir_all(parent)?; + } + let file = File::create(LOCK_PATH)?; + let fd = { + use std::os::unix::io::AsRawFd; + file.as_raw_fd() + }; + let ret = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) }; + if ret != 0 { + return Err("dump already in progress".into()); + } + Ok(Self { _file: file }) + } +} + +impl Drop for FlockGuard { + fn drop(&mut self) { + // flock released automatically when file descriptor closes + } +} + +fn random_name(len: usize) -> String { + use rand::Rng; + let mut rng = rand::thread_rng(); + (0..len) + .map(|_| { + let idx = rng.gen_range(0..36u8); + if idx < 10 { + (b'0' + idx) as char + } else { + (b'a' + idx - 10) as char + } + }) + .collect() +} + +fn collect_logcat(tag: &str) -> Vec { + let output = Command::new("logcat") + .args(["-d", "-s", tag]) + .output(); + + match output { + Ok(o) => { + let mut data = o.stdout; + data.truncate(LOGCAT_SIZE_LIMIT); + data + } + Err(_) => Vec::new(), + } +} + +fn collect_device_info() -> String { + let mut info = String::new(); + + if let Ok(output) = Command::new("uname").arg("-a").output() { + info.push_str(&format!( + "uname={}\n", + String::from_utf8_lossy(&output.stdout).trim() + )); + } + + for (key, prop) in [ + ("device", "ro.product.device"), + ("build", "ro.build.display.id"), + ("android", "ro.build.version.release"), + ] { + if let Ok(output) = Command::new("getprop").arg(prop).output() { + info.push_str(&format!( + "{}={}\n", + key, + String::from_utf8_lossy(&output.stdout).trim() + )); + } + } + + // KSU version + if let Ok(ver) = fs::read_to_string("/data/adb/ksu/version") { + info.push_str(&format!("ksu={}\n", ver.trim())); + } + + // Module version from module.prop + if let Ok(prop) = fs::read_to_string("/data/adb/modules/tricky_store/module.prop") { + for line in prop.lines() { + if let Some(ver) = line.strip_prefix("version=") { + info.push_str(&format!("module={}\n", ver.trim())); + break; + } + } + } + + info +} + +fn read_file_bytes(path: &str) -> Option> { + let mut buf = Vec::new(); + File::open(path).ok()?.read_to_end(&mut buf).ok()?; + Some(buf) +} + +fn epoch_millis() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) +} + +pub fn execute_dump() -> Result<(), Box> { + let _lock = FlockGuard::acquire()?; + + let _ = fs::create_dir_all(DUMP_DIR); + let zip_name = format!("{}.zip", random_name(8)); + let zip_path = format!("{}/{}", DUMP_DIR, zip_name); + + let zip_file = File::create(&zip_path)?; + let mut zip = zip::ZipWriter::new(zip_file); + let options = + zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated); + + let mut file_count = 0u32; + + // Log files + let log_files = [ + "certgen.log", + "certgen.log.1", + "certgen.log.2", + "certgen.log.3", + "certgen.log.4", + ]; + for name in &log_files { + let path = format!("{}/{}", LOG_DIR, name); + if let Some(data) = read_file_bytes(&path) { + zip.start_file(*name, options)?; + zip.write_all(&data)?; + file_count += 1; + } + } + + // Logcat + let logcat = collect_logcat("TEESimulator"); + if !logcat.is_empty() { + zip.start_file("logcat-teesimulator.log", options)?; + zip.write_all(&logcat)?; + file_count += 1; + } + + // Config files + for name in ["tee_status.txt", "security_patch.txt"] { + let path = format!("{}/{}", BASE_DIR, name); + if let Some(data) = read_file_bytes(&path) { + zip.start_file(name, options)?; + zip.write_all(&data)?; + file_count += 1; + } + } + + // Device info + let device_info = collect_device_info(); + if !device_info.is_empty() { + zip.start_file("device-info.txt", options)?; + zip.write_all(device_info.as_bytes())?; + file_count += 1; + } + + // Manifest + let manifest = serde_json::json!({ + "timestamp": epoch_millis(), + "version": env!("CARGO_PKG_VERSION"), + "files": file_count, + }); + zip.start_file("manifest.json", options)?; + zip.write_all(manifest.to_string().as_bytes())?; + + zip.finish()?; + + let zip_size = fs::metadata(&zip_path).map(|m| m.len()).unwrap_or(0); + fs::write(DUMP_PATH_FILE, &zip_path)?; + + let result = serde_json::json!({ + "zip": zip_path, + "size": zip_size, + "files": file_count + 1, // +1 for manifest + }); + println!("{}", result); + + tracing::info!(path = %zip_path, size = zip_size, "diagnostic dump created"); + + Ok(()) +} diff --git a/native-certgen/src/logging/kmsg.rs b/native-certgen/src/logging/kmsg.rs new file mode 100644 index 0000000..b1cd41b --- /dev/null +++ b/native-certgen/src/logging/kmsg.rs @@ -0,0 +1,93 @@ +use std::fs::{File, OpenOptions}; +use std::io::Write; +use std::sync::Mutex; +use tracing::field::{Field, Visit}; +use tracing::{Event, Level, Subscriber}; +use tracing_subscriber::layer::Context; +use tracing_subscriber::Layer; + +const KMSG_PATH: &str = "/dev/kmsg"; +const TAG: &str = "TEESimulator"; + +pub struct KmsgLayer { + writer: Mutex>, +} + +impl KmsgLayer { + pub fn new() -> Self { + let file = OpenOptions::new().write(true).open(KMSG_PATH).ok(); + Self { + writer: Mutex::new(file), + } + } +} + +fn syslog_priority(level: &Level) -> u8 { + match *level { + Level::ERROR => 3, + Level::WARN => 4, + Level::INFO => 6, + Level::DEBUG | Level::TRACE => 7, + } +} + +struct MessageVisitor { + message: String, + fields: String, +} + +impl MessageVisitor { + fn new() -> Self { + Self { + message: String::new(), + fields: String::new(), + } + } +} + +impl Visit for MessageVisitor { + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + if field.name() == "message" { + let raw = format!("{:?}", value); + // Strip surrounding debug quotes if present + self.message = raw + .strip_prefix('"') + .and_then(|s| s.strip_suffix('"')) + .unwrap_or(&raw) + .to_string(); + } else { + if !self.fields.is_empty() { + self.fields.push(' '); + } + self.fields.push_str(&format!("{}={:?}", field.name(), value)); + } + } +} + +impl Layer for KmsgLayer { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let mut guard = match self.writer.lock() { + Ok(g) => g, + Err(_) => return, + }; + let file = match guard.as_mut() { + Some(f) => f, + None => return, + }; + + let priority = syslog_priority(event.metadata().level()); + let mut visitor = MessageVisitor::new(); + event.record(&mut visitor); + + let line = if visitor.fields.is_empty() { + format!("<{}>{}: {}\n", priority, TAG, visitor.message) + } else { + format!( + "<{}>{}: {} {}\n", + priority, TAG, visitor.message, visitor.fields + ) + }; + + let _ = file.write_all(line.as_bytes()); + } +} diff --git a/native-certgen/src/logging/mod.rs b/native-certgen/src/logging/mod.rs new file mode 100644 index 0000000..3e770ca --- /dev/null +++ b/native-certgen/src/logging/mod.rs @@ -0,0 +1,40 @@ +mod kmsg; +mod rotating; +pub mod sysfs; +pub mod dump; + +use std::path::Path; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose"; + +pub fn init( + verbose_flag: bool, + log_dir: &str, + max_size_mb: u64, + max_files: usize, +) -> Result<(), Box> { + let verbose = verbose_flag || Path::new(VERBOSE_MARKER).exists(); + + let (max_size, max_files) = if verbose { + (5 * 1024 * 1024, 5) + } else { + (max_size_mb * 1024 * 1024, max_files) + }; + + let level = if verbose { "trace" } else { "info" }; + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level)); + + let kmsg_layer = kmsg::KmsgLayer::new(); + let rotating_layer = rotating::RotatingFileLayer::new(log_dir, max_size, max_files); + let stderr_layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr); + + tracing_subscriber::registry() + .with(filter) + .with(kmsg_layer) + .with(rotating_layer) + .with(stderr_layer) + .try_init()?; + + Ok(()) +} diff --git a/native-certgen/src/logging/rotating.rs b/native-certgen/src/logging/rotating.rs new file mode 100644 index 0000000..743e544 --- /dev/null +++ b/native-certgen/src/logging/rotating.rs @@ -0,0 +1,167 @@ +use std::fs::{self, File, OpenOptions}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::time::{SystemTime, UNIX_EPOCH}; +use tracing::field::{Field, Visit}; +use tracing::{Event, Level, Subscriber}; +use tracing_subscriber::layer::Context; +use tracing_subscriber::Layer; + +struct RotatingState { + dir: PathBuf, + current: Option, + current_size: u64, + max_size: u64, + max_files: usize, +} + +pub struct RotatingFileLayer { + state: Mutex, +} + +impl RotatingFileLayer { + pub fn new(dir: &str, max_size: u64, max_files: usize) -> Self { + let dir = PathBuf::from(dir); + let _ = fs::create_dir_all(&dir); + let (file, size) = open_current_log(&dir); + Self { + state: Mutex::new(RotatingState { + dir, + current: file, + current_size: size, + max_size, + max_files, + }), + } + } +} + +fn open_current_log(dir: &Path) -> (Option, u64) { + let path = dir.join("certgen.log"); + let size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0); + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .ok(); + (file, size) +} + +fn rotate(state: &mut RotatingState) { + // Close current handle before renaming + state.current.take(); + + let dir = &state.dir; + // Shift older files up: .{max-1} is deleted, .{N} -> .{N+1} + for i in (1..state.max_files).rev() { + let from = dir.join(format!("certgen.log.{}", i)); + let to = dir.join(format!("certgen.log.{}", i + 1)); + if from.exists() { + let _ = fs::rename(&from, &to); + } + } + // Current -> .1 + let current_path = dir.join("certgen.log"); + let first_rotated = dir.join("certgen.log.1"); + if current_path.exists() { + let _ = fs::rename(¤t_path, &first_rotated); + } + + // Delete excess files beyond max_files + let excess = dir.join(format!("certgen.log.{}", state.max_files + 1)); + if excess.exists() { + let _ = fs::remove_file(&excess); + } + + let (file, size) = open_current_log(dir); + state.current = file; + state.current_size = size; +} + +fn epoch_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} + +fn level_str(level: &Level) -> &'static str { + match *level { + Level::ERROR => "ERROR", + Level::WARN => "WARN", + Level::INFO => "INFO", + Level::DEBUG => "DEBUG", + Level::TRACE => "TRACE", + } +} + +struct LogVisitor { + message: String, + fields: String, +} + +impl LogVisitor { + fn new() -> Self { + Self { + message: String::new(), + fields: String::new(), + } + } +} + +impl Visit for LogVisitor { + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + if field.name() == "message" { + let raw = format!("{:?}", value); + self.message = raw + .strip_prefix('"') + .and_then(|s| s.strip_suffix('"')) + .unwrap_or(&raw) + .to_string(); + } else { + if !self.fields.is_empty() { + self.fields.push(' '); + } + self.fields.push_str(&format!("{}={:?}", field.name(), value)); + } + } +} + +impl Layer for RotatingFileLayer { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let mut state = match self.state.lock() { + Ok(s) => s, + Err(_) => return, + }; + + if state.current_size >= state.max_size { + rotate(&mut state); + } + + let file = match state.current.as_mut() { + Some(f) => f, + None => return, + }; + + let ts = epoch_secs(); + let lvl = level_str(event.metadata().level()); + let target = event.metadata().target(); + + let mut visitor = LogVisitor::new(); + event.record(&mut visitor); + + let line = if visitor.fields.is_empty() { + format!("{} [{}] {}: {}\n", ts, lvl, target, visitor.message) + } else { + format!( + "{} [{}] {}: {} {}\n", + ts, lvl, target, visitor.message, visitor.fields + ) + }; + + if file.write_all(line.as_bytes()).is_ok() { + state.current_size += line.len() as u64; + } + } +} diff --git a/native-certgen/src/logging/sysfs.rs b/native-certgen/src/logging/sysfs.rs new file mode 100644 index 0000000..a96307c --- /dev/null +++ b/native-certgen/src/logging/sysfs.rs @@ -0,0 +1,38 @@ +use std::fs; +use std::path::Path; + +const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose"; + +pub fn is_verbose() -> bool { + Path::new(VERBOSE_MARKER).exists() +} + +pub fn set_verbose_marker(enabled: bool) -> Result<(), Box> { + if enabled { + if let Some(parent) = Path::new(VERBOSE_MARKER).parent() { + fs::create_dir_all(parent)?; + } + fs::write(VERBOSE_MARKER, "")?; + } else if Path::new(VERBOSE_MARKER).exists() { + fs::remove_file(VERBOSE_MARKER)?; + } + Ok(()) +} + +pub fn enable() -> Result<(), Box> { + set_verbose_marker(true)?; + tracing::info!("verbose logging enabled via marker file"); + Ok(()) +} + +pub fn disable() -> Result<(), Box> { + set_verbose_marker(false)?; + tracing::info!("verbose logging disabled, marker file removed"); + Ok(()) +} + +pub fn status() -> Result<(), Box> { + let state = if is_verbose() { "enabled" } else { "disabled" }; + tracing::info!(verbose = state, "verbose marker status"); + Ok(()) +}