zipfuse: Support mounting as noexec
Add an option to mount while disallowing the execution of binary files.
This allows for a layer of defence when mounting something that is only
expected to contain data. Although the data may still be treated as a
script, the interpreter for that script will have to come from another
source.
The `zipfuse` command takes the `--noexec` flag to enable this option.
Bug: 237742605
Test: atest ZipFuseTest
Change-Id: I732c96109ceaf65716c2c3ac27862c65bb7bd81f
diff --git a/zipfuse/src/main.rs b/zipfuse/src/main.rs
index c3fae69..874056a 100644
--- a/zipfuse/src/main.rs
+++ b/zipfuse/src/main.rs
@@ -46,6 +46,12 @@
.required(false)
.help("Comma separated list of mount options"),
)
+ .arg(
+ Arg::with_name("noexec")
+ .long("noexec")
+ .takes_value(false)
+ .help("Disallow the execution of binary files"),
+ )
.arg(Arg::with_name("ZIPFILE").required(true))
.arg(Arg::with_name("MOUNTPOINT").required(true))
.get_matches();
@@ -53,12 +59,18 @@
let zip_file = matches.value_of("ZIPFILE").unwrap().as_ref();
let mount_point = matches.value_of("MOUNTPOINT").unwrap().as_ref();
let options = matches.value_of("options");
- run_fuse(zip_file, mount_point, options)?;
+ let noexec = matches.is_present("noexec");
+ run_fuse(zip_file, mount_point, options, noexec)?;
Ok(())
}
/// Runs a fuse filesystem by mounting `zip_file` on `mount_point`.
-pub fn run_fuse(zip_file: &Path, mount_point: &Path, extra_options: Option<&str>) -> Result<()> {
+pub fn run_fuse(
+ zip_file: &Path,
+ mount_point: &Path,
+ extra_options: Option<&str>,
+ noexec: bool,
+) -> Result<()> {
const MAX_READ: u32 = 1 << 20; // TODO(jiyong): tune this
const MAX_WRITE: u32 = 1 << 13; // This is a read-only filesystem
@@ -76,12 +88,12 @@
mount_options.push(MountOption::Extra(value));
}
- fuse::mount(
- mount_point,
- "zipfuse",
- libc::MS_NOSUID | libc::MS_NODEV | libc::MS_RDONLY,
- &mount_options,
- )?;
+ let mut mount_flags = libc::MS_NOSUID | libc::MS_NODEV | libc::MS_RDONLY;
+ if noexec {
+ mount_flags |= libc::MS_NOEXEC;
+ }
+
+ fuse::mount(mount_point, "zipfuse", mount_flags, &mount_options)?;
let mut config = fuse::FuseConfig::new();
config.dev_fuse(dev_fuse).max_write(MAX_WRITE).max_read(MAX_READ);
Ok(config.enter_message_loop(ZipFuse::new(zip_file)?)?)
@@ -435,22 +447,28 @@
use zip::write::FileOptions;
#[cfg(not(target_os = "android"))]
- fn start_fuse(zip_path: &Path, mnt_path: &Path) {
+ fn start_fuse(zip_path: &Path, mnt_path: &Path, noexec: bool) {
let zip_path = PathBuf::from(zip_path);
let mnt_path = PathBuf::from(mnt_path);
std::thread::spawn(move || {
- crate::run_fuse(&zip_path, &mnt_path, None).unwrap();
+ crate::run_fuse(&zip_path, &mnt_path, None, noexec).unwrap();
});
}
#[cfg(target_os = "android")]
- fn start_fuse(zip_path: &Path, mnt_path: &Path) {
+ fn start_fuse(zip_path: &Path, mnt_path: &Path, noexec: bool) {
// Note: for some unknown reason, running a thread to serve fuse doesn't work on Android.
// Explicitly spawn a zipfuse process instead.
// TODO(jiyong): fix this
+ let noexec = if noexec { "--noexec" } else { "" };
assert!(std::process::Command::new("sh")
.arg("-c")
- .arg(format!("/data/local/tmp/zipfuse {} {}", zip_path.display(), mnt_path.display()))
+ .arg(format!(
+ "/data/local/tmp/zipfuse {} {} {}",
+ noexec,
+ zip_path.display(),
+ mnt_path.display()
+ ))
.spawn()
.is_ok());
}
@@ -476,6 +494,14 @@
// Creates a zip file, adds some files to the zip file, mounts it using zipfuse, runs the check
// routine, and finally unmounts.
fn run_test(add: fn(&mut zip::ZipWriter<File>), check: fn(&std::path::Path)) {
+ run_test_noexec(false, add, check);
+ }
+
+ fn run_test_noexec(
+ noexec: bool,
+ add: fn(&mut zip::ZipWriter<File>),
+ check: fn(&std::path::Path),
+ ) {
// Create an empty zip file
let test_dir = tempfile::TempDir::new().unwrap();
let zip_path = test_dir.path().join("test.zip");
@@ -492,7 +518,7 @@
let mnt_path = test_dir.path().join("mnt");
assert!(fs::create_dir(&mnt_path).is_ok());
- start_fuse(&zip_path, &mnt_path);
+ start_fuse(&zip_path, &mnt_path, noexec);
let mnt_path = test_dir.path().join("mnt");
// Give some time for the fuse to boot up
@@ -577,6 +603,26 @@
}
#[test]
+ fn noexec() {
+ fn add_executable(zip: &mut zip::ZipWriter<File>) {
+ zip.start_file("executable", FileOptions::default().unix_permissions(0o755)).unwrap();
+ }
+
+ // Executables can be run when not mounting with noexec.
+ run_test(add_executable, |root| {
+ let res = std::process::Command::new(root.join("executable")).status();
+ res.unwrap();
+ });
+
+ // Mounting with noexec results in permissions denial when running an executable.
+ let noexec = true;
+ run_test_noexec(noexec, add_executable, |root| {
+ let res = std::process::Command::new(root.join("executable")).status();
+ assert!(matches!(res.unwrap_err().kind(), std::io::ErrorKind::PermissionDenied));
+ });
+ }
+
+ #[test]
fn single_dir() {
run_test(
|zip| {
@@ -688,7 +734,8 @@
let mnt_path = test_dir.join("mnt");
assert!(fs::create_dir(&mnt_path).is_ok());
- start_fuse(zip_path, &mnt_path);
+ let noexec = false;
+ start_fuse(zip_path, &mnt_path, noexec);
// Give some time for the fuse to boot up
assert!(wait_for_mount(&mnt_path).is_ok());