blob: f5318c8dfebb0dc650b17185381fa6822a48258d [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.
*/
mod caller;
use super::{Endpoint, channel};
use crate::windows::{checked, owned};
pub use caller::Caller;
use serde::{Serialize, de::DeserializeOwned};
use std::{
io,
mem::size_of,
os::windows::{
ffi::OsStrExt,
io::{AsHandle, AsRawHandle, OwnedHandle},
},
path::{Path, PathBuf},
ptr,
time::Duration,
};
use tokio::net::windows::named_pipe::{ClientOptions, NamedPipeClient};
use windows_sys::Win32::{
System::{
Com::{COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE, CoInitializeEx, CoUninitialize},
Pipes::GetNamedPipeServerProcessId,
Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW},
},
UI::Shell::{
SEE_MASK_FLAG_NO_UI, SEE_MASK_NO_CONSOLE, SEE_MASK_NOASYNC, SEE_MASK_NOCLOSEPROCESS,
SHELLEXECUTEINFOW, ShellExecuteExW,
},
};
pub const ELEVATED_SETUP: &str = "__maka-sandbox-setup";
/// A user-authorized, one-shot helper, not a privileged command service.
/// Its request handler must own durable recovery before performing OS effects.
/// Once delivered, accepted work may finish even if its caller disconnects.
/// The caller owns the operation deadline, including consent and completion.
pub async fn administrative<Request: Serialize, Response: DeserializeOwned>(
executable: &Path,
request: &Request,
) -> io::Result<Response> {
let endpoint = Endpoint::administrative()?;
let executable = executable.to_owned();
let id = endpoint.id();
let (sender, receiver) = tokio::sync::oneshot::channel();
// ShellExecute's consent dialog may wait for a human. A dedicated short-
// lived STA, rather than Tokio's blocking pool, keeps runtime shutdown and
// unrelated work independent of that wait. A late helper sees a closed pipe.
std::thread::Builder::new()
.name("sandbox-consent".into())
.spawn(move || {
let _ = sender.send(start(&executable, id));
})?;
let process = receiver.await.map_err(io::Error::other)??;
let mut connection = endpoint.accept(process.as_handle()).await?;
tokio::time::timeout(Duration::from_secs(15), connection.send(request))
.await
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "sandbox setup was not admitted"))??;
connection.receive().await
}
pub struct AdministrativeRequest<T> {
pub value: T,
pub caller: Caller,
connection: NamedPipeClient,
}
impl<T> AdministrativeRequest<T> {
pub async fn respond(self, response: &impl Serialize) -> io::Result<()> {
let mut connection = self.connection;
tokio::time::timeout(
Duration::from_secs(15),
channel::send(&mut connection, response),
)
.await
.map_err(|_| {
io::Error::new(
io::ErrorKind::TimedOut,
"sandbox setup reply was not collected",
)
})?
}
}
/// The elevated helper accepts exactly one bounded request from the captured
/// Host process running the same executable. No authority is taken from argv
/// beyond the random local rendezvous and the expected caller identity.
pub async fn receive_administrative<T: DeserializeOwned>(
endpoint: uuid::Uuid,
expected_host: u32,
) -> io::Result<AdministrativeRequest<T>> {
let host = owned(unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, expected_host) })?;
let mut image = vec![0u16; 32768];
let mut length = image.len() as u32;
checked(unsafe {
QueryFullProcessImageNameW(host.as_raw_handle(), 0, image.as_mut_ptr(), &mut length)
})?;
let image =
PathBuf::from(String::from_utf16(&image[..length as usize]).map_err(io::Error::other)?);
if dunce::canonicalize(image)? != dunce::canonicalize(std::env::current_exe()?)? {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"unexpected setup caller executable",
));
}
let caller = Caller::capture(host.as_handle())?;
let mut connection = ClientOptions::new().open(channel::name(endpoint))?;
let mut server = 0;
checked(unsafe { GetNamedPipeServerProcessId(connection.as_raw_handle(), &mut server) })?;
if server != expected_host {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"unexpected setup caller peer",
));
}
let value = tokio::time::timeout(Duration::from_secs(15), channel::receive(&mut connection))
.await
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "sandbox setup was not admitted"))??;
Ok(AdministrativeRequest {
value,
caller,
connection,
})
}
fn start(executable: &Path, endpoint: uuid::Uuid) -> io::Result<OwnedHandle> {
if !executable.is_absolute() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"setup executable must be absolute",
));
}
let result = unsafe {
CoInitializeEx(
ptr::null(),
(COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) as u32,
)
};
if result < 0 {
return Err(io::Error::other(format!(
"initialize consent apartment: HRESULT 0x{:08x}",
result as u32
)));
}
struct Apartment;
impl Drop for Apartment {
fn drop(&mut self) {
unsafe { CoUninitialize() };
}
}
let _apartment = Apartment;
let executable = wide(executable.as_os_str())?;
let parameters = wide(std::ffi::OsStr::new(&format!(
"{ELEVATED_SETUP} {endpoint} {}",
std::process::id()
)))?;
let verb = wide(std::ffi::OsStr::new("runas"))?;
let mut request = SHELLEXECUTEINFOW {
cbSize: size_of::<SHELLEXECUTEINFOW>() as u32,
fMask: SEE_MASK_NOCLOSEPROCESS
| SEE_MASK_NOASYNC
| SEE_MASK_FLAG_NO_UI
| SEE_MASK_NO_CONSOLE,
lpVerb: verb.as_ptr(),
lpFile: executable.as_ptr(),
lpParameters: parameters.as_ptr(),
nShow: 0,
..Default::default()
};
checked(unsafe { ShellExecuteExW(&mut request) })?;
owned(request.hProcess)
}
fn wide(value: &std::ffi::OsStr) -> io::Result<Vec<u16>> {
let mut value: Vec<_> = value.encode_wide().collect();
if value.contains(&0) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"NUL in setup launch",
));
}
value.push(0);
Ok(value)
}