| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| use anyhow::{anyhow, bail, Context, Result}; |
| use log::{debug, error, info, warn}; |
| use minijail::{self, Minijail}; |
| use regex::Regex; |
| use std::env; |
| use std::ffi::OsString; |
| use std::fs::{read_dir, File}; |
| use std::os::unix::io::{AsRawFd, RawFd}; |
| use std::path::{self, Path, PathBuf}; |
| use std::process::Command; |
| |
| use crate::artifact_signer::ArtifactSigner; |
| use crate::compos_key_service::Signer; |
| use crate::fsverity; |
| use authfs_aidl_interface::aidl::com::android::virt::fs::{ |
| AuthFsConfig::{ |
| AuthFsConfig, InputDirFdAnnotation::InputDirFdAnnotation, |
| InputFdAnnotation::InputFdAnnotation, OutputDirFdAnnotation::OutputDirFdAnnotation, |
| OutputFdAnnotation::OutputFdAnnotation, |
| }, |
| IAuthFs::IAuthFs, |
| IAuthFsService::IAuthFsService, |
| }; |
| use authfs_aidl_interface::binder::{ParcelFileDescriptor, Strong}; |
| use compos_aidl_interface::aidl::com::android::compos::FdAnnotation::FdAnnotation; |
| use compos_common::odrefresh::ExitCode; |
| |
| const FD_SERVER_PORT: i32 = 3264; // TODO: support dynamic port |
| |
| /// The number that represents the file descriptor number expecting by the task. The number may be |
| /// meaningless in the current process. |
| pub type PseudoRawFd = i32; |
| |
| pub enum CompilerOutput { |
| /// Fs-verity digests of output files, if the compiler finishes successfully. |
| Digests { |
| oat: fsverity::Sha256Digest, |
| vdex: fsverity::Sha256Digest, |
| image: fsverity::Sha256Digest, |
| }, |
| /// Exit code returned by the compiler, if not 0. |
| ExitCode(i8), |
| } |
| |
| struct CompilerOutputParcelFds { |
| oat: ParcelFileDescriptor, |
| vdex: ParcelFileDescriptor, |
| image: ParcelFileDescriptor, |
| } |
| |
| pub struct OdrefreshContext<'a> { |
| system_dir_fd: i32, |
| output_dir_fd: i32, |
| staging_dir_fd: i32, |
| target_dir_name: &'a str, |
| zygote_arch: &'a str, |
| system_server_compiler_filter: &'a str, |
| } |
| |
| impl<'a> OdrefreshContext<'a> { |
| pub fn new( |
| system_dir_fd: i32, |
| output_dir_fd: i32, |
| staging_dir_fd: i32, |
| target_dir_name: &'a str, |
| zygote_arch: &'a str, |
| system_server_compiler_filter: &'a str, |
| ) -> Result<Self> { |
| if system_dir_fd < 0 || output_dir_fd < 0 || staging_dir_fd < 0 { |
| bail!("The remote FDs are expected to be non-negative"); |
| } |
| if !matches!(zygote_arch, "zygote64" | "zygote64_32") { |
| bail!("Invalid zygote arch"); |
| } |
| // Disallow any sort of path traversal |
| if target_dir_name.contains(path::MAIN_SEPARATOR) { |
| bail!("Invalid target directory {}", target_dir_name); |
| } |
| |
| // We're not validating/allowlisting the compiler filter, and just assume the compiler will |
| // reject an invalid string. We need to accept "verify" filter anyway, and potential |
| // performance degration by the attacker is not currently in scope. This also allows ART to |
| // specify new compiler filter and configure through system property without change to |
| // CompOS. |
| |
| Ok(Self { |
| system_dir_fd, |
| output_dir_fd, |
| staging_dir_fd, |
| target_dir_name, |
| zygote_arch, |
| system_server_compiler_filter, |
| }) |
| } |
| } |
| |
| pub fn odrefresh( |
| odrefresh_path: &Path, |
| context: OdrefreshContext, |
| authfs_service: Strong<dyn IAuthFsService>, |
| signer: Signer, |
| ) -> Result<ExitCode> { |
| // Mount authfs (via authfs_service). The authfs instance unmounts once the `authfs` variable |
| // is out of scope. |
| let authfs_config = AuthFsConfig { |
| port: FD_SERVER_PORT, |
| inputDirFdAnnotations: vec![InputDirFdAnnotation { |
| fd: context.system_dir_fd, |
| // TODO(206869687): Replace /dev/null with the real path when possible. |
| manifestPath: "/dev/null".to_string(), |
| prefix: "/system".to_string(), |
| }], |
| outputDirFdAnnotations: vec![ |
| OutputDirFdAnnotation { fd: context.output_dir_fd }, |
| OutputDirFdAnnotation { fd: context.staging_dir_fd }, |
| ], |
| ..Default::default() |
| }; |
| let authfs = authfs_service.mount(&authfs_config)?; |
| let mountpoint = PathBuf::from(authfs.getMountPoint()?); |
| |
| let mut android_root = mountpoint.clone(); |
| android_root.push(context.system_dir_fd.to_string()); |
| android_root.push("system"); |
| env::set_var("ANDROID_ROOT", &android_root); |
| debug!("ANDROID_ROOT={:?}", &android_root); |
| |
| let art_apex_data = mountpoint.join(context.output_dir_fd.to_string()); |
| env::set_var("ART_APEX_DATA", &art_apex_data); |
| debug!("ART_APEX_DATA={:?}", &art_apex_data); |
| |
| let staging_dir = mountpoint.join(context.staging_dir_fd.to_string()); |
| |
| set_classpaths(&android_root)?; |
| |
| let mut args = vec![ |
| "odrefresh".to_string(), |
| format!("--zygote-arch={}", context.zygote_arch), |
| format!("--dalvik-cache={}", context.target_dir_name), |
| format!("--staging-dir={}", staging_dir.display()), |
| "--no-refresh".to_string(), |
| ]; |
| |
| if !context.system_server_compiler_filter.is_empty() { |
| args.push(format!( |
| "--system-server-compiler-filter={}", |
| context.system_server_compiler_filter |
| )); |
| } |
| args.push("--force-compile".to_string()); |
| |
| debug!("Running odrefresh with args: {:?}", &args); |
| let jail = spawn_jailed_task(odrefresh_path, &args, Vec::new() /* fd_mapping */) |
| .context("Spawn odrefresh")?; |
| let exit_code = match jail.wait() { |
| Ok(_) => Result::<u8>::Ok(0), |
| Err(minijail::Error::ReturnCode(exit_code)) => Ok(exit_code), |
| Err(e) => { |
| bail!("Unexpected minijail error: {}", e) |
| } |
| }?; |
| |
| let exit_code = ExitCode::from_i32(exit_code.into())?; |
| info!("odrefresh exited with {:?}", exit_code); |
| |
| if exit_code == ExitCode::CompilationSuccess { |
| // authfs only shows us the files we created, so it's ok to just sign everything under |
| // the target directory. |
| let target_dir = art_apex_data.join(context.target_dir_name); |
| let mut artifact_signer = ArtifactSigner::new(&target_dir); |
| add_artifacts(&target_dir, &mut artifact_signer)?; |
| |
| artifact_signer.write_info_and_signature(signer, &target_dir.join("compos.info"))?; |
| } |
| |
| Ok(exit_code) |
| } |
| |
| fn set_classpaths(android_root: &Path) -> Result<()> { |
| let export_lines = run_derive_classpath(android_root)?; |
| load_classpath_vars(&export_lines) |
| } |
| |
| fn run_derive_classpath(android_root: &Path) -> Result<String> { |
| let classpaths_root = android_root.join("etc/classpaths"); |
| |
| let mut bootclasspath_arg = OsString::new(); |
| bootclasspath_arg.push("--bootclasspath-fragment="); |
| bootclasspath_arg.push(classpaths_root.join("bootclasspath.pb")); |
| |
| let mut systemserverclasspath_arg = OsString::new(); |
| systemserverclasspath_arg.push("--systemserverclasspath-fragment="); |
| systemserverclasspath_arg.push(classpaths_root.join("systemserverclasspath.pb")); |
| |
| let result = Command::new("/apex/com.android.sdkext/bin/derive_classpath") |
| .arg(bootclasspath_arg) |
| .arg(systemserverclasspath_arg) |
| .arg("/proc/self/fd/1") |
| .output() |
| .context("Failed to run derive_classpath")?; |
| |
| if !result.status.success() { |
| bail!("derive_classpath returned {}", result.status); |
| } |
| |
| String::from_utf8(result.stdout).context("Converting derive_classpath output") |
| } |
| |
| fn load_classpath_vars(export_lines: &str) -> Result<()> { |
| // Each line should be in the format "export <var name> <value>" |
| let pattern = Regex::new(r"^export ([^ ]+) ([^ ]+)$").context("Failed to construct Regex")?; |
| for line in export_lines.lines() { |
| if let Some(captures) = pattern.captures(line) { |
| let name = &captures[1]; |
| let value = &captures[2]; |
| // TODO(b/213416778) Don't modify our env, construct a fresh one for odrefresh |
| env::set_var(name, value); |
| } else { |
| warn!("Malformed line from derive_classpath: {}", line); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn add_artifacts(target_dir: &Path, artifact_signer: &mut ArtifactSigner) -> Result<()> { |
| for entry in |
| read_dir(&target_dir).with_context(|| format!("Traversing {}", target_dir.display()))? |
| { |
| let entry = entry?; |
| let file_type = entry.file_type()?; |
| if file_type.is_dir() { |
| add_artifacts(&entry.path(), artifact_signer)?; |
| } else if file_type.is_file() { |
| artifact_signer.add_artifact(&entry.path())?; |
| } else { |
| // authfs shouldn't create anything else, but just in case |
| bail!("Unexpected file type in artifacts: {:?}", entry); |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Runs the compiler with given flags with file descriptors described in `fd_annotation` retrieved |
| /// via `authfs_service`. Returns exit code of the compiler process. |
| pub fn compile_cmd( |
| compiler_path: &Path, |
| compiler_args: &[String], |
| authfs_service: Strong<dyn IAuthFsService>, |
| fd_annotation: &FdAnnotation, |
| ) -> Result<CompilerOutput> { |
| // Mount authfs (via authfs_service). The authfs instance unmounts once the `authfs` variable |
| // is out of scope. |
| let authfs_config = build_authfs_config(fd_annotation); |
| let authfs = authfs_service.mount(&authfs_config)?; |
| |
| // The task expects to receive FD numbers that match its flags (e.g. --zip-fd=42) prepared |
| // on the host side. Since the local FD opened from authfs (e.g. /authfs/42) may not match |
| // the task's expectation, prepare a FD mapping and let minijail prepare the correct FD |
| // setup. |
| let fd_mapping = |
| open_authfs_files_for_fd_mapping(&authfs, &authfs_config).context("Open on authfs")?; |
| |
| let jail = |
| spawn_jailed_task(compiler_path, compiler_args, fd_mapping).context("Spawn dex2oat")?; |
| let jail_result = jail.wait(); |
| |
| let parcel_fds = parse_compiler_args(&authfs, compiler_args)?; |
| let oat_file: &File = parcel_fds.oat.as_ref(); |
| let vdex_file: &File = parcel_fds.vdex.as_ref(); |
| let image_file: &File = parcel_fds.image.as_ref(); |
| |
| match jail_result { |
| Ok(()) => Ok(CompilerOutput::Digests { |
| oat: fsverity::measure(oat_file.as_raw_fd())?, |
| vdex: fsverity::measure(vdex_file.as_raw_fd())?, |
| image: fsverity::measure(image_file.as_raw_fd())?, |
| }), |
| Err(minijail::Error::ReturnCode(exit_code)) => { |
| error!("dex2oat failed with exit code {}", exit_code); |
| Ok(CompilerOutput::ExitCode(exit_code as i8)) |
| } |
| Err(e) => { |
| bail!("Unexpected minijail error: {}", e) |
| } |
| } |
| } |
| |
| fn parse_compiler_args( |
| authfs: &Strong<dyn IAuthFs>, |
| args: &[String], |
| ) -> Result<CompilerOutputParcelFds> { |
| const OAT_FD_PREFIX: &str = "--oat-fd="; |
| const VDEX_FD_PREFIX: &str = "--output-vdex-fd="; |
| const IMAGE_FD_PREFIX: &str = "--image-fd="; |
| const APP_IMAGE_FD_PREFIX: &str = "--app-image-fd="; |
| |
| let mut oat = None; |
| let mut vdex = None; |
| let mut image = None; |
| |
| for arg in args { |
| if let Some(value) = arg.strip_prefix(OAT_FD_PREFIX) { |
| let fd = value.parse::<RawFd>().context("Invalid --oat-fd flag")?; |
| debug_assert!(oat.is_none()); |
| oat = Some(authfs.openFile(fd, false)?); |
| } else if let Some(value) = arg.strip_prefix(VDEX_FD_PREFIX) { |
| let fd = value.parse::<RawFd>().context("Invalid --output-vdex-fd flag")?; |
| debug_assert!(vdex.is_none()); |
| vdex = Some(authfs.openFile(fd, false)?); |
| } else if let Some(value) = arg.strip_prefix(IMAGE_FD_PREFIX) { |
| let fd = value.parse::<RawFd>().context("Invalid --image-fd flag")?; |
| debug_assert!(image.is_none()); |
| image = Some(authfs.openFile(fd, false)?); |
| } else if let Some(value) = arg.strip_prefix(APP_IMAGE_FD_PREFIX) { |
| let fd = value.parse::<RawFd>().context("Invalid --app-image-fd flag")?; |
| debug_assert!(image.is_none()); |
| image = Some(authfs.openFile(fd, false)?); |
| } |
| } |
| |
| Ok(CompilerOutputParcelFds { |
| oat: oat.ok_or_else(|| anyhow!("Missing --oat-fd"))?, |
| vdex: vdex.ok_or_else(|| anyhow!("Missing --vdex-fd"))?, |
| image: image.ok_or_else(|| anyhow!("Missing --image-fd or --app-image-fd"))?, |
| }) |
| } |
| |
| fn build_authfs_config(fd_annotation: &FdAnnotation) -> AuthFsConfig { |
| AuthFsConfig { |
| port: FD_SERVER_PORT, |
| inputFdAnnotations: fd_annotation |
| .input_fds |
| .iter() |
| .map(|fd| InputFdAnnotation { fd: *fd }) |
| .collect(), |
| outputFdAnnotations: fd_annotation |
| .output_fds |
| .iter() |
| .map(|fd| OutputFdAnnotation { fd: *fd }) |
| .collect(), |
| ..Default::default() |
| } |
| } |
| |
| fn open_authfs_files_for_fd_mapping( |
| authfs: &Strong<dyn IAuthFs>, |
| config: &AuthFsConfig, |
| ) -> Result<Vec<(ParcelFileDescriptor, PseudoRawFd)>> { |
| let mut fd_mapping = Vec::new(); |
| |
| let results: Result<Vec<_>> = config |
| .inputFdAnnotations |
| .iter() |
| .map(|annotation| Ok((authfs.openFile(annotation.fd, false)?, annotation.fd))) |
| .collect(); |
| fd_mapping.append(&mut results?); |
| |
| let results: Result<Vec<_>> = config |
| .outputFdAnnotations |
| .iter() |
| .map(|annotation| Ok((authfs.openFile(annotation.fd, true)?, annotation.fd))) |
| .collect(); |
| fd_mapping.append(&mut results?); |
| |
| Ok(fd_mapping) |
| } |
| |
| fn spawn_jailed_task( |
| executable: &Path, |
| args: &[String], |
| fd_mapping: Vec<(ParcelFileDescriptor, PseudoRawFd)>, |
| ) -> Result<Minijail> { |
| // TODO(b/185175567): Run in a more restricted sandbox. |
| let jail = Minijail::new()?; |
| let preserve_fds: Vec<_> = fd_mapping.iter().map(|(f, id)| (f.as_raw_fd(), *id)).collect(); |
| let _pid = jail.run_remap(executable, preserve_fds.as_slice(), args)?; |
| Ok(jail) |
| } |