Skip to content

Commit 1eb4e22

Browse files
committed
feat(core): harden VFS injection, crash resilience, and mount fallback
VFS executor now pre-injects directory rules for missing ancestor paths before leaf file rules, ensuring modules that target non-existent intermediate directories (e.g., /vendor/lib64/soundfx/) resolve correctly through both getname_flags and permission hooks. Module scanner rejects IDs with path traversal characters at the system boundary. Conflict detection between modules promoted to warn-level logging for operational visibility. module.prop writes use atomic tmp+rename to prevent corruption on OOM kill or watchdog timeout. Panic hook and mount-error handler surface crash context in the KSU/APatch manager description field. Overlay executor falls back to magic mount per-module when overlay fails on all partitions for a given module, preventing one bad module from taking down all mounts. Reflink (FICLONE) copy added for f2fs CoW acceleration during overlay staging.
1 parent 16e706f commit 1eb4e22

8 files changed

Lines changed: 209 additions & 11 deletions

File tree

src/main.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ fn main() -> Result<()> {
4141
// the process before any visible work (R08: fixes BUG-L3).
4242
let cli = Cli::parse();
4343

44+
std::panic::set_hook(Box::new(|info| {
45+
let msg = info
46+
.payload()
47+
.downcast_ref::<&str>()
48+
.copied()
49+
.or_else(|| info.payload().downcast_ref::<String>().map(|s| s.as_str()))
50+
.unwrap_or("unknown panic");
51+
52+
let loc = info
53+
.location()
54+
.map(|l| format!(" ({}:{})", l.file(), l.line()))
55+
.unwrap_or_default();
56+
57+
let desc = format!("❌ Crashed: {msg}{loc}");
58+
eprintln!("zeromount panic: {msg}{loc}");
59+
let _ = utils::platform::write_description_to_module_prop(&desc);
60+
}));
61+
4462
if let Err(e) = utils::process::camouflage() {
4563
eprintln!("camouflage failed (non-fatal): {e}");
4664
}
@@ -49,7 +67,9 @@ fn main() -> Result<()> {
4967
logging::init(cli.verbose, &config.logging)?;
5068
utils::signal::register_shutdown_handler();
5169

52-
match cli.command {
70+
let is_mount = matches!(cli.command, Commands::Mount);
71+
72+
let result = match cli.command {
5373
Commands::Mount => cli::handlers::handle_mount(),
5474
Commands::Detect => cli::handlers::handle_detect(),
5575
Commands::Status { json } => cli::handlers::handle_status(json),
@@ -75,5 +95,14 @@ fn main() -> Result<()> {
7595
println!("zeromount v{}", read_version_from_prop());
7696
Ok(())
7797
}
98+
};
99+
100+
if let Err(ref e) = result {
101+
if is_mount {
102+
let desc = format!("❌ Mount failed: {e:#}");
103+
let _ = utils::platform::write_description_to_module_prop(&desc);
104+
}
78105
}
106+
107+
result
79108
}

src/modules/rules.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::collections::HashMap;
22
use std::path::Path;
33

4-
use tracing::debug;
4+
use tracing::warn;
55

66
use crate::core::types::{ModuleFileType, ScannedModule};
77

@@ -27,7 +27,7 @@ pub fn detect_conflicts(modules: &[ScannedModule]) -> u32 {
2727
let mut conflict_count = 0u32;
2828
for (path, providers) in &file_map {
2929
if providers.len() > 1 {
30-
debug!(
30+
warn!(
3131
path = %path.display(),
3232
modules = %providers.join(", "),
3333
"file conflict: multiple modules provide same path"
@@ -37,7 +37,7 @@ pub fn detect_conflicts(modules: &[ScannedModule]) -> u32 {
3737
}
3838

3939
if conflict_count > 0 {
40-
debug!(count = conflict_count, "file conflicts detected");
40+
warn!(count = conflict_count, "file conflicts detected");
4141
}
4242

4343
conflict_count

src/modules/scanner.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,14 @@ pub fn scan_modules(modules_dir: &Path) -> Result<Vec<ScannedModule>> {
5050
Some(n) => n,
5151
None => return false,
5252
};
53-
!BLACKLISTED_NAMES.contains(&name)
53+
if BLACKLISTED_NAMES.contains(&name) {
54+
return false;
55+
}
56+
if name.contains("..") || !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-') {
57+
warn!(id = name, "rejected module with invalid ID");
58+
return false;
59+
}
60+
true
5461
})
5562
.filter(|p| is_module_enabled(p) && !has_skip_mount(p) && !has_manual_mounts(p))
5663
.collect();

src/mount/executor.rs

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,13 @@ fn execute_overlay(
8787
staged.push((pm, lower_dirs));
8888
}
8989

90-
// Phase 2: All staging succeeded mount overlays.
90+
// Phase 2: All staging succeeded -- mount overlays.
9191
// Lower dirs are partition-level (e.g., .../viperfxmod/system/) but mount points
9292
// may be subdirectories (e.g., /system/etc). Append the relative suffix so overlay
9393
// only exposes files belonging to that mount point.
9494
let mut results = Vec::new();
95+
let mut failed_module_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
96+
let mut succeeded_module_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
9597

9698
for (pm, lower_dirs) in &staged {
9799
let adjusted: Vec<PathBuf> = lower_dirs
@@ -119,9 +121,22 @@ fn execute_overlay(
119121
let decoy_ref = decoy_subdir.as_deref();
120122

121123
let result = match mount_overlay(&lower_refs, target, &mount_id, &storage.overlay_source, decoy_ref) {
122-
Ok(r) => r,
124+
Ok(r) => {
125+
for mid in &pm.contributing_modules {
126+
succeeded_module_ids.insert(mid.clone());
127+
}
128+
r
129+
}
123130
Err(e) => {
124-
warn!(target = %target.display(), error = %e, "overlay mount failed");
131+
warn!(
132+
target = %target.display(),
133+
modules = ?pm.contributing_modules,
134+
error = %e,
135+
"overlay mount failed, modules queued for magic mount fallback"
136+
);
137+
for mid in &pm.contributing_modules {
138+
failed_module_ids.insert(mid.clone());
139+
}
125140
MountResult {
126141
module_id: mount_id.clone(),
127142
strategy_used: MountStrategy::Overlay,
@@ -143,10 +158,52 @@ fn execute_overlay(
143158
decoy::teardown_decoy(d);
144159
}
145160

161+
// Phase 3: Fallback to magic mount for modules that failed overlay on every
162+
// partition they contributed to. Modules with at least one successful overlay
163+
// are excluded to prevent double-mounting.
164+
let fallback_ids: Vec<&str> = failed_module_ids
165+
.iter()
166+
.filter(|id| !succeeded_module_ids.contains(id.as_str()))
167+
.map(|id| id.as_str())
168+
.collect();
169+
170+
if !fallback_ids.is_empty() {
171+
let fallback_modules: Vec<&ScannedModule> = fallback_ids
172+
.iter()
173+
.filter_map(|id| module_map.get(id).copied())
174+
.collect();
175+
176+
warn!(
177+
count = fallback_modules.len(),
178+
modules = ?fallback_ids,
179+
"falling back to magic mount for overlay-failed modules"
180+
);
181+
182+
match execute_magic_mount_for(
183+
&fallback_modules,
184+
capabilities,
185+
mount_config,
186+
) {
187+
Ok(mut fallback_results) => results.append(&mut fallback_results),
188+
Err(e) => {
189+
warn!(error = %e, "magic mount fallback failed");
190+
}
191+
}
192+
}
193+
146194
info!(mounts = results.len(), "overlay execution complete");
147195
Ok(results)
148196
}
149197

198+
fn execute_magic_mount_for(
199+
modules: &[&ScannedModule],
200+
capabilities: &CapabilityFlags,
201+
mount_config: &MountConfig,
202+
) -> Result<Vec<MountResult>> {
203+
let owned: Vec<ScannedModule> = modules.iter().map(|m| (*m).clone()).collect();
204+
execute_magic_mount(&owned, capabilities, mount_config)
205+
}
206+
150207
fn execute_magic_mount(
151208
modules: &[ScannedModule],
152209
capabilities: &CapabilityFlags,
@@ -216,7 +273,7 @@ fn prepare_lower_dir(
216273
ensure_parent_dirs_with_context(lower_dir, parent, partition)?;
217274
}
218275
if src.exists() {
219-
fs::copy(&src, &dst).with_context(|| {
276+
crate::utils::fs::copy_file(&src, &dst).with_context(|| {
220277
format!("copy {} -> {}", src.display(), dst.display())
221278
})?;
222279
crate::utils::selinux::copy_selinux_context(&src, &dst);

src/utils/fs.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use std::fs;
2+
use std::io;
3+
use std::os::unix::io::AsRawFd;
4+
use std::path::Path;
5+
6+
const FICLONE: libc::c_ulong = 0x40049409;
7+
8+
pub fn copy_file(src: &Path, dst: &Path) -> io::Result<u64> {
9+
let src_file = fs::File::open(src)?;
10+
let dst_file = fs::File::create(dst)?;
11+
12+
// SAFETY: Both fds are valid open files. FICLONE is a well-defined ioctl
13+
// that performs CoW reflink on supporting filesystems (f2fs, btrfs, xfs).
14+
// On unsupported filesystems it returns EOPNOTSUPP/EXDEV/EINVAL.
15+
let ret = unsafe { libc::ioctl(dst_file.as_raw_fd(), FICLONE, src_file.as_raw_fd()) };
16+
17+
if ret == 0 {
18+
let meta = src_file.metadata()?;
19+
let perms = meta.permissions();
20+
dst_file.set_permissions(perms)?;
21+
return Ok(meta.len());
22+
}
23+
24+
drop(src_file);
25+
drop(dst_file);
26+
fs::copy(src, dst)
27+
}

src/utils/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod command;
2+
pub mod fs;
23
pub mod hash;
34
pub mod lock;
45
pub mod platform;

src/utils/platform.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,15 @@ pub(crate) fn write_description_to_module_prop(text: &str) -> Result<()> {
167167
updated.push('\n');
168168
}
169169

170-
std::fs::write(&prop_path, &updated)
171-
.context("failed to write module.prop for description update")?;
170+
let tmp_path = prop_path.with_extension("prop.tmp");
171+
std::fs::write(&tmp_path, &updated)
172+
.context("failed to write module.prop.tmp for description update")?;
173+
174+
if let Err(e) = std::fs::rename(&tmp_path, &prop_path) {
175+
tracing::warn!("atomic rename failed, trying direct write: {e}");
176+
std::fs::write(&prop_path, &updated)
177+
.context("failed to write module.prop for description update")?;
178+
}
172179
Ok(())
173180
}
174181

src/vfs/executor.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashSet;
12
use std::path::{Path, PathBuf};
23

34
use anyhow::{Context, Result};
@@ -56,6 +57,56 @@ impl VfsExecutor {
5657
Ok(results)
5758
}
5859

60+
fn ensure_ancestor_dirs(
61+
&self,
62+
relative: &Path,
63+
module: &ScannedModule,
64+
injected_dirs: &mut HashSet<PathBuf>,
65+
mount_paths: &mut Vec<String>,
66+
) -> (u32, u32) {
67+
let mut applied = 0u32;
68+
let mut failed = 0u32;
69+
let mut missing = Vec::new();
70+
71+
let mut ancestor = relative.parent();
72+
while let Some(rel) = ancestor {
73+
if rel.as_os_str().is_empty() {
74+
break;
75+
}
76+
let target = match resolve_target_path(rel) {
77+
Some(t) => t,
78+
None => break,
79+
};
80+
if injected_dirs.contains(&target) || target.exists() {
81+
break;
82+
}
83+
missing.push((rel.to_path_buf(), target));
84+
ancestor = rel.parent();
85+
}
86+
87+
for (rel, target) in missing.into_iter().rev() {
88+
let source = module.path.join(&rel);
89+
if !source.exists() {
90+
continue;
91+
}
92+
if let Err(e) = self.driver.add_rule(&target, &source, true) {
93+
debug!(
94+
module = %module.id,
95+
target = %target.display(),
96+
error = %e,
97+
"ancestor dir rule failed"
98+
);
99+
failed += 1;
100+
continue;
101+
}
102+
injected_dirs.insert(target.clone());
103+
mount_paths.push(target.display().to_string());
104+
applied += 1;
105+
}
106+
107+
(applied, failed)
108+
}
109+
59110
fn inject_module_rules(
60111
&self,
61112
module: &ScannedModule,
@@ -65,6 +116,7 @@ impl VfsExecutor {
65116
let mut failed = 0u32;
66117
let mut mount_paths = Vec::new();
67118
let mut error = None;
119+
let mut injected_dirs = HashSet::new();
68120

69121
for file in &module.files {
70122
// Whiteouts → hide the original path via SUSFS
@@ -128,10 +180,17 @@ impl VfsExecutor {
128180
None => continue,
129181
};
130182
if target.exists() {
183+
injected_dirs.insert(target);
131184
continue;
132185
}
186+
let (a, f) = self.ensure_ancestor_dirs(
187+
&file.relative_path, module, &mut injected_dirs, &mut mount_paths,
188+
);
189+
applied += a;
190+
failed += f;
133191
match self.driver.add_rule(&target, &source, true) {
134192
Ok(()) => {
193+
injected_dirs.insert(target.clone());
135194
applied += 1;
136195
mount_paths.push(target.display().to_string());
137196
}
@@ -153,6 +212,11 @@ impl VfsExecutor {
153212
ModuleFileType::OpaqueDir => {
154213
let source = module.path.join(&file.relative_path);
155214
if let Some(target) = resolve_target_path(&file.relative_path) {
215+
let (a, f) = self.ensure_ancestor_dirs(
216+
&file.relative_path, module, &mut injected_dirs, &mut mount_paths,
217+
);
218+
applied += a;
219+
failed += f;
156220
match self.driver.add_rule(&target, &source, true) {
157221
Ok(()) => {
158222
applied += 1;
@@ -188,6 +252,12 @@ impl VfsExecutor {
188252
}
189253
};
190254

255+
let (a, f) = self.ensure_ancestor_dirs(
256+
&file.relative_path, module, &mut injected_dirs, &mut mount_paths,
257+
);
258+
applied += a;
259+
failed += f;
260+
191261
match self.driver.add_rule(&target, &source, false) {
192262
Ok(()) => {
193263
applied += 1;

0 commit comments

Comments
 (0)