blob: d42303d7c3846c12f142dd7286ec74d5eb545899 [file] [log] [blame]
// Copyright 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.
//! This module is intended for testing access control enforcement of services such as keystore2,
//! by assuming various identities with varying levels of privilege. Consequently, appropriate
//! privileges are required, or the attempt will fail causing a panic.
//! The `run_as` module provides the function `run_as`, which takes a UID, GID, an SELinux
//! context, and a closure. The return type of the closure, which is also the return type of
//! `run_as`, must implement `serde::Serialize` and `serde::Deserialize`.
//! `run_as` forks, transitions to the given identity, and executes the closure in the newly
//! forked process. If the closure returns, i.e., does not panic, the forked process exits with
//! a status of `0`, and the return value is serialized and sent through a pipe to the parent where
//! it gets deserialized and returned. The STDIO is not changed and the parent's panic handler
//! remains unchanged. So if the closure panics, the panic message is printed on the parent's STDERR
//! and the exit status is set to a non `0` value. The latter causes the parent to panic as well,
//! and if run in a test context, the test to fail.
use keystore2_selinux as selinux;
use nix::sys::wait::{waitpid, WaitStatus};
use nix::unistd::{
close, fork, pipe as nix_pipe, read as nix_read, setgid, setuid, write as nix_write,
ForkResult, Gid, Uid,
};
use serde::{de::DeserializeOwned, Serialize};
use std::os::unix::io::RawFd;
fn transition(se_context: selinux::Context, uid: Uid, gid: Gid) {
setgid(gid).expect("Failed to set GID. This test might need more privileges.");
setuid(uid).expect("Failed to set UID. This test might need more privileges.");
selinux::setcon(&se_context)
.expect("Failed to set SELinux context. This test might need more privileges.");
}
/// PipeReader is a simple wrapper around raw pipe file descriptors.
/// It takes ownership of the file descriptor and closes it on drop. It provides `read_all`, which
/// reads from the pipe into an expending vector, until no more data can be read.
struct PipeReader(RawFd);
impl PipeReader {
pub fn read_all(&self) -> Result<Vec<u8>, nix::Error> {
let mut buffer = [0u8; 128];
let mut result = Vec::<u8>::new();
loop {
let bytes = nix_read(self.0, &mut buffer)?;
if bytes == 0 {
return Ok(result);
}
result.extend_from_slice(&buffer[0..bytes]);
}
}
}
impl Drop for PipeReader {
fn drop(&mut self) {
close(self.0).expect("Failed to close reader pipe fd.");
}
}
/// PipeWriter is a simple wrapper around raw pipe file descriptors.
/// It takes ownership of the file descriptor and closes it on drop. It provides `write`, which
/// writes the given buffer into the pipe, returning the number of bytes written.
struct PipeWriter(RawFd);
impl PipeWriter {
pub fn write(&self, data: &[u8]) -> Result<usize, nix::Error> {
nix_write(self.0, data)
}
}
impl Drop for PipeWriter {
fn drop(&mut self) {
close(self.0).expect("Failed to close writer pipe fd.");
}
}
fn pipe() -> Result<(PipeReader, PipeWriter), nix::Error> {
let (read_fd, write_fd) = nix_pipe()?;
Ok((PipeReader(read_fd), PipeWriter(write_fd)))
}
/// Run the given closure in a new process running with the new identity given as
/// `uid`, `gid`, and `se_context`.
pub fn run_as<F, R>(se_context: &str, uid: Uid, gid: Gid, f: F) -> R
where
R: Serialize + DeserializeOwned,
F: 'static + Send + FnOnce() -> R,
{
let se_context =
selinux::Context::new(se_context).expect("Unable to construct selinux::Context.");
let (reader, writer) = pipe().expect("Failed to create pipe.");
match unsafe { fork() } {
Ok(ForkResult::Parent { child, .. }) => {
drop(writer);
let status = waitpid(child, None).expect("Failed while waiting for child.");
if let WaitStatus::Exited(_, 0) = status {
// Child exited successfully.
// Read the result from the pipe.
let serialized_result =
reader.read_all().expect("Failed to read result from child.");
// Deserialize the result and return it.
serde_cbor::from_slice(&serialized_result).expect("Failed to deserialize result.")
} else {
panic!("Child did not exit as expected {:?}", status);
}
}
Ok(ForkResult::Child) => {
// This will panic on error or insufficient privileges.
transition(se_context, uid, gid);
// Run the closure.
let result = f();
// Serialize the result of the closure.
let vec = serde_cbor::to_vec(&result).expect("Result serialization failed");
// Send the result to the parent using the pipe.
writer.write(&vec).expect("Failed to send serialized result to parent.");
// Set exit status to `0`.
std::process::exit(0);
}
Err(errno) => {
panic!("Failed to fork: {:?}", errno);
}
}
}
#[cfg(test)]
mod test {
use super::*;
use keystore2_selinux as selinux;
use nix::unistd::{getgid, getuid};
use serde::{Deserialize, Serialize};
/// This test checks that the closure does not produce an exit status of `0` when run inside a
/// test and the closure panics. This would mask test failures as success.
#[test]
#[should_panic]
fn test_run_as_panics_on_closure_panic() {
run_as(selinux::getcon().unwrap().to_str().unwrap(), getuid(), getgid(), || {
panic!("Closure must panic.")
});
}
static TARGET_UID: Uid = Uid::from_raw(10020);
static TARGET_GID: Gid = Gid::from_raw(10020);
static TARGET_CTX: &str = "u:r:untrusted_app:s0:c91,c256,c10,c20";
/// Tests that the closure is running as the target identity.
#[test]
fn test_transition_to_untrusted_app() {
run_as(TARGET_CTX, TARGET_UID, TARGET_GID, || {
assert_eq!(TARGET_UID, getuid());
assert_eq!(TARGET_GID, getgid());
assert_eq!(TARGET_CTX, selinux::getcon().unwrap().to_str().unwrap());
});
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct SomeResult {
a: u32,
b: u64,
c: String,
}
#[test]
fn test_serialized_result() {
let test_result = SomeResult {
a: 5,
b: 0xffffffffffffffff,
c: "supercalifragilisticexpialidocious".to_owned(),
};
let test_result_clone = test_result.clone();
let result = run_as(TARGET_CTX, TARGET_UID, TARGET_GID, || test_result_clone);
assert_eq!(test_result, result);
}
}