blob: ca01bb62f8442b04c72ae6157ecb3ecb61884479 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 crate::windows::{checked, job::Job, owned, wait_process};
use maka_sandbox::windows::{Account, Desktop, ExecutionJob, Password};
use std::{
fs::File,
io,
mem::size_of,
os::windows::{
ffi::OsStrExt,
io::{AsHandle, AsRawHandle, BorrowedHandle, OwnedHandle},
},
path::{Path, PathBuf},
process::ExitStatus,
sync::Arc,
};
use windows_sys::Win32::System::Threading::*;
pub const RUNNER: &str = "__maka-sandbox-runner";
/// Captured, already-authorized installation resources. Construction does not
/// start a process; the execution owner calls start only after admission.
pub struct Identity {
pub account: Account,
pub password: Password,
pub desktop: Arc<Desktop>,
pub job: Arc<ExecutionJob>,
pub leases: Vec<File>,
}
/// Retain for the entire command tree, not merely until the bootstrap replies.
/// Dropping it kills its tree while preserving the installation's other work.
pub struct Runner {
process: OwnedHandle,
pub(crate) job: Job,
pid: u32,
_desktop: Arc<Desktop>,
_execution: Arc<ExecutionJob>,
_leases: Vec<File>,
cleanup: crate::command::Cleanup,
proxy: Option<Arc<maka_network::proxy::Proxy>>,
closing_proxy: Option<maka_network::proxy::Proxy>,
}
impl Identity {
pub async fn start(self, executable: PathBuf, endpoint: uuid::Uuid) -> io::Result<Runner> {
tokio::task::spawn_blocking(move || self.start_native(&executable, endpoint))
.await
.map_err(io::Error::other)?
}
fn start_native(self, executable: &Path, endpoint: uuid::Uuid) -> io::Result<Runner> {
if !executable.is_absolute() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"runner executable must be absolute",
));
}
let executable = wide(executable.as_os_str())?;
// WithLogon has a 1024 UTF-16 command-line limit. Command bodies and
// credentials never enter argv: only this random local endpoint does.
let mut line = wide(std::ffi::OsStr::new(&format!(
"maka {RUNNER} {endpoint} {}",
std::process::id()
)))?;
let account = wide(std::ffi::OsStr::new(&self.account.name()))?;
let domain = [b'.' as u16, 0];
let mut desktop = wide(std::ffi::OsStr::new(self.desktop.name()))?;
let system = std::env::var_os("SystemRoot")
.ok_or_else(|| io::Error::other("SystemRoot is missing"))?;
let cwd = wide(&system)?;
// Trusted bootstrap does not inherit the Host's secrets or controls.
let mut environment = wide(std::ffi::OsStr::new(&format!(
"SystemRoot={}",
system.to_string_lossy()
)))?;
environment.push(0);
let startup = STARTUPINFOW {
cb: size_of::<STARTUPINFOW>() as u32,
lpDesktop: desktop.as_mut_ptr(),
..Default::default()
};
let job = Job(self.job.as_handle().try_clone_to_owned()?);
let mut process = PROCESS_INFORMATION::default();
checked(unsafe {
CreateProcessWithLogonW(
account.as_ptr(),
domain.as_ptr(),
self.password.as_wide().as_ptr(),
0,
executable.as_ptr(),
line.as_mut_ptr(),
CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW,
environment.as_ptr().cast(),
cwd.as_ptr(),
&startup,
&mut process,
)
})
.map_err(|error| context("create account runner", error))?;
let process_handle = owned(process.hProcess)?;
let thread = owned(process.hThread)?;
// The runner remains suspended until its unique command-tree Job owns it.
// Secondary Logon may already have assigned a service-owned parent.
let result = (|| {
self.job
.assign(process_handle.as_handle())
.map_err(|error| context("attach execution Job", error))?;
if unsafe { ResumeThread(thread.as_raw_handle()) } == u32::MAX {
return Err(io::Error::last_os_error());
}
Ok(())
})();
if let Err(error) = result {
unsafe {
TerminateProcess(process_handle.as_raw_handle(), 130);
WaitForSingleObject(process_handle.as_raw_handle(), 5_000);
}
return Err(error);
}
Ok(Runner {
process: process_handle,
job,
pid: process.dwProcessId,
_desktop: self.desktop,
_execution: self.job,
_leases: self.leases,
cleanup: crate::command::Cleanup::Complete,
proxy: None,
closing_proxy: None,
})
}
}
impl Runner {
pub fn with_proxy(mut self, proxy: Option<Arc<maka_network::proxy::Proxy>>) -> Self {
self.proxy = proxy;
self
}
/// Install the executor's durable settlement action before publishing this
/// runner. Native tree drain always precedes revoking its permissions.
pub fn with_cleanup(
mut self,
cleanup: impl FnOnce() -> io::Result<()> + Send + Sync + 'static,
) -> Self {
self.cleanup = crate::command::Cleanup::new(cleanup);
self
}
pub async fn finish(&mut self) -> io::Result<()> {
self.terminate()?;
tokio::time::timeout(std::time::Duration::from_secs(2), async {
while !self.job.is_empty()? {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
Ok::<_, io::Error>(())
})
.await
.map_err(|_| io::Error::other("sandbox runner tree exit is unconfirmed"))??;
if let Some(proxy) = self.proxy.take() {
self.closing_proxy = Arc::into_inner(proxy);
}
if let Some(proxy) = &mut self.closing_proxy {
proxy.close().await?;
self.closing_proxy = None;
}
self.cleanup.finish().await
}
pub fn id(&self) -> u32 {
self.pid
}
pub async fn wait(&self) -> io::Result<ExitStatus> {
wait_process(&self.process).await
}
pub fn terminate(&self) -> io::Result<()> {
self.job.terminate(130)
}
}
impl AsHandle for Runner {
fn as_handle(&self) -> BorrowedHandle<'_> {
self.process.as_handle()
}
}
impl Drop for Runner {
fn drop(&mut self) {
let _ = self.terminate();
}
}
fn wide(value: &std::ffi::OsStr) -> io::Result<Vec<u16>> {
let mut result: Vec<_> = value.encode_wide().collect();
if result.contains(&0) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "embedded NUL"));
}
result.push(0);
Ok(result)
}
fn context(operation: &str, error: io::Error) -> io::Error {
io::Error::new(error.kind(), format!("{operation}: {error}"))
}