blob: d42303d7c3846c12f142dd7286ec74d5eb545899 [file] [log] [blame]
Janis Danisevskisa578d392021-09-20 15:44:06 -07001// Copyright 2021, The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! This module is intended for testing access control enforcement of services such as keystore2,
16//! by assuming various identities with varying levels of privilege. Consequently, appropriate
17//! privileges are required, or the attempt will fail causing a panic.
18//! The `run_as` module provides the function `run_as`, which takes a UID, GID, an SELinux
19//! context, and a closure. The return type of the closure, which is also the return type of
20//! `run_as`, must implement `serde::Serialize` and `serde::Deserialize`.
21//! `run_as` forks, transitions to the given identity, and executes the closure in the newly
22//! forked process. If the closure returns, i.e., does not panic, the forked process exits with
23//! a status of `0`, and the return value is serialized and sent through a pipe to the parent where
24//! it gets deserialized and returned. The STDIO is not changed and the parent's panic handler
25//! remains unchanged. So if the closure panics, the panic message is printed on the parent's STDERR
26//! and the exit status is set to a non `0` value. The latter causes the parent to panic as well,
27//! and if run in a test context, the test to fail.
28
29use keystore2_selinux as selinux;
30use nix::sys::wait::{waitpid, WaitStatus};
31use nix::unistd::{
32 close, fork, pipe as nix_pipe, read as nix_read, setgid, setuid, write as nix_write,
33 ForkResult, Gid, Uid,
34};
35use serde::{de::DeserializeOwned, Serialize};
36use std::os::unix::io::RawFd;
37
38fn transition(se_context: selinux::Context, uid: Uid, gid: Gid) {
39 setgid(gid).expect("Failed to set GID. This test might need more privileges.");
40 setuid(uid).expect("Failed to set UID. This test might need more privileges.");
41
42 selinux::setcon(&se_context)
43 .expect("Failed to set SELinux context. This test might need more privileges.");
44}
45
46/// PipeReader is a simple wrapper around raw pipe file descriptors.
47/// It takes ownership of the file descriptor and closes it on drop. It provides `read_all`, which
48/// reads from the pipe into an expending vector, until no more data can be read.
49struct PipeReader(RawFd);
50
51impl PipeReader {
52 pub fn read_all(&self) -> Result<Vec<u8>, nix::Error> {
53 let mut buffer = [0u8; 128];
54 let mut result = Vec::<u8>::new();
55 loop {
56 let bytes = nix_read(self.0, &mut buffer)?;
57 if bytes == 0 {
58 return Ok(result);
59 }
60 result.extend_from_slice(&buffer[0..bytes]);
61 }
62 }
63}
64
65impl Drop for PipeReader {
66 fn drop(&mut self) {
67 close(self.0).expect("Failed to close reader pipe fd.");
68 }
69}
70
71/// PipeWriter is a simple wrapper around raw pipe file descriptors.
72/// It takes ownership of the file descriptor and closes it on drop. It provides `write`, which
73/// writes the given buffer into the pipe, returning the number of bytes written.
74struct PipeWriter(RawFd);
75
76impl PipeWriter {
77 pub fn write(&self, data: &[u8]) -> Result<usize, nix::Error> {
78 nix_write(self.0, data)
79 }
80}
81
82impl Drop for PipeWriter {
83 fn drop(&mut self) {
84 close(self.0).expect("Failed to close writer pipe fd.");
85 }
86}
87
88fn pipe() -> Result<(PipeReader, PipeWriter), nix::Error> {
89 let (read_fd, write_fd) = nix_pipe()?;
90 Ok((PipeReader(read_fd), PipeWriter(write_fd)))
91}
92
93/// Run the given closure in a new process running with the new identity given as
94/// `uid`, `gid`, and `se_context`.
95pub fn run_as<F, R>(se_context: &str, uid: Uid, gid: Gid, f: F) -> R
96where
97 R: Serialize + DeserializeOwned,
98 F: 'static + Send + FnOnce() -> R,
99{
100 let se_context =
101 selinux::Context::new(se_context).expect("Unable to construct selinux::Context.");
102 let (reader, writer) = pipe().expect("Failed to create pipe.");
103
104 match unsafe { fork() } {
105 Ok(ForkResult::Parent { child, .. }) => {
106 drop(writer);
107 let status = waitpid(child, None).expect("Failed while waiting for child.");
108 if let WaitStatus::Exited(_, 0) = status {
109 // Child exited successfully.
110 // Read the result from the pipe.
111 let serialized_result =
112 reader.read_all().expect("Failed to read result from child.");
113
114 // Deserialize the result and return it.
115 serde_cbor::from_slice(&serialized_result).expect("Failed to deserialize result.")
116 } else {
117 panic!("Child did not exit as expected {:?}", status);
118 }
119 }
120 Ok(ForkResult::Child) => {
121 // This will panic on error or insufficient privileges.
122 transition(se_context, uid, gid);
123
124 // Run the closure.
125 let result = f();
126
127 // Serialize the result of the closure.
128 let vec = serde_cbor::to_vec(&result).expect("Result serialization failed");
129
130 // Send the result to the parent using the pipe.
131 writer.write(&vec).expect("Failed to send serialized result to parent.");
132
133 // Set exit status to `0`.
134 std::process::exit(0);
135 }
136 Err(errno) => {
137 panic!("Failed to fork: {:?}", errno);
138 }
139 }
140}
141
142#[cfg(test)]
143mod test {
144 use super::*;
145 use keystore2_selinux as selinux;
146 use nix::unistd::{getgid, getuid};
147 use serde::{Deserialize, Serialize};
148
149 /// This test checks that the closure does not produce an exit status of `0` when run inside a
150 /// test and the closure panics. This would mask test failures as success.
151 #[test]
152 #[should_panic]
153 fn test_run_as_panics_on_closure_panic() {
154 run_as(selinux::getcon().unwrap().to_str().unwrap(), getuid(), getgid(), || {
155 panic!("Closure must panic.")
156 });
157 }
158
159 static TARGET_UID: Uid = Uid::from_raw(10020);
160 static TARGET_GID: Gid = Gid::from_raw(10020);
161 static TARGET_CTX: &str = "u:r:untrusted_app:s0:c91,c256,c10,c20";
162
163 /// Tests that the closure is running as the target identity.
164 #[test]
165 fn test_transition_to_untrusted_app() {
166 run_as(TARGET_CTX, TARGET_UID, TARGET_GID, || {
167 assert_eq!(TARGET_UID, getuid());
168 assert_eq!(TARGET_GID, getgid());
169 assert_eq!(TARGET_CTX, selinux::getcon().unwrap().to_str().unwrap());
170 });
171 }
172
173 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
174 struct SomeResult {
175 a: u32,
176 b: u64,
177 c: String,
178 }
179
180 #[test]
181 fn test_serialized_result() {
182 let test_result = SomeResult {
183 a: 5,
184 b: 0xffffffffffffffff,
185 c: "supercalifragilisticexpialidocious".to_owned(),
186 };
187 let test_result_clone = test_result.clone();
188 let result = run_as(TARGET_CTX, TARGET_UID, TARGET_GID, || test_result_clone);
189 assert_eq!(test_result, result);
190 }
191}