mod stop;
pub(crate) use stop::StopJob;
mod retain;
mod spawn;
mod tcp;
use crate::core::error::{AppError, Result};
use crate::native::process::Pinned;
pub use retain::Retained;
use serde::{Deserialize, Serialize};
pub use spawn::{SpawnSpec, Spawned};
use std::time::Instant;
pub use tcp::BoundStream;
use windows::Win32::Foundation::{
CloseHandle, ERROR_ALREADY_EXISTS, ERROR_SUCCESS, GetLastError, HANDLE, HLOCAL, LocalFree,
SetLastError,
};
use windows::Win32::Security::Authorization::{
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
};
use windows::Win32::Security::{PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES};
use windows::Win32::System::JobObjects::*;
use windows::core::{PCWSTR, w};
const PREFIX: &str = "Local\\qq-cli-";
const QUERY: u32 = 0x0004; const MAX_MEMBERS: usize = 256;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct JobId(String);
impl JobId {
pub(crate) fn validate(&self) -> Result<()> {
self.wide().map(|_| ())
}
fn wide(&self) -> Result<Vec<u16>> {
let suffix = self.0.strip_prefix(PREFIX).ok_or_else(unavailable)?;
if suffix.len() != 64
|| !suffix
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
{
return Err(unavailable());
}
Ok(self.0.encode_utf16().chain([0]).collect())
}
}
struct OwnedHandle(HANDLE);
impl Drop for OwnedHandle {
fn drop(&mut self) {
let _ = unsafe { CloseHandle(self.0) };
}
}
fn pin(handle: OwnedHandle, deadline: Instant) -> Result<Pinned> {
let raw = std::mem::ManuallyDrop::new(handle).0;
let handle =
unsafe { winsafe::guard::CloseHandleGuard::new(winsafe::HPROCESS::from_ptr(raw.0)) };
Pinned::from_handle(handle, deadline)
}
struct Descriptor(PSECURITY_DESCRIPTOR);
impl Drop for Descriptor {
fn drop(&mut self) {
let _ = unsafe { LocalFree(Some(HLOCAL(self.0.0))) };
}
}
pub struct Job {
id: JobId,
handle: OwnedHandle,
creator: bool,
}
impl Job {
pub fn create(deadline: Instant) -> Result<Self> {
crate::core::deadline::check(deadline)?;
let mut nonce = [0u8; 32];
getrandom::fill(&mut nonce).map_err(|_| unavailable())?;
let id = JobId(format!(
"{PREFIX}{}",
nonce.iter().map(|b| format!("{b:02x}")).collect::<String>()
));
Self::create_named(id, deadline)
}
fn create_named(id: JobId, deadline: Instant) -> Result<Self> {
crate::core::deadline::check(deadline)?;
let name = id.wide()?;
let mut descriptor = PSECURITY_DESCRIPTOR::default();
unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
w!("D:P(A;;GA;;;SY)(A;;GA;;;OW)"),
SDDL_REVISION_1,
&mut descriptor,
None,
)
}
.map_err(|_| unavailable())?;
let descriptor = Descriptor(descriptor);
let attributes = SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: descriptor.0.0,
bInheritHandle: false.into(),
};
unsafe { SetLastError(ERROR_SUCCESS) };
let handle = unsafe { CreateJobObjectW(Some(&attributes), PCWSTR(name.as_ptr())) }
.map_err(|_| unavailable())?;
let existed = unsafe { GetLastError() } == ERROR_ALREADY_EXISTS;
let job = Self {
id,
handle: OwnedHandle(handle),
creator: true,
};
if existed {
return Err(unavailable());
}
job.check_limits(deadline)?;
Ok(job)
}
pub fn reopen(id: &JobId, deadline: Instant) -> Result<Self> {
crate::core::deadline::check(deadline)?;
let name = id.wide()?;
let handle = unsafe { OpenJobObjectW(QUERY, false, PCWSTR(name.as_ptr())) }
.map_err(|_| unavailable())?;
let job = Self {
id: id.clone(),
handle: OwnedHandle(handle),
creator: false,
};
job.check_limits(deadline)?;
Ok(job)
}
pub fn id(&self) -> &JobId {
&self.id
}
fn check_limits(&self, deadline: Instant) -> Result<()> {
crate::core::deadline::check(deadline)?;
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
unsafe {
QueryInformationJobObject(
Some(self.handle.0),
JobObjectExtendedLimitInformation,
(&mut info as *mut JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
std::mem::size_of_val(&info) as u32,
None,
)
}
.map_err(|_| unavailable())?;
if info.BasicLimitInformation.LimitFlags.0 != 0 {
return Err(unavailable());
}
crate::core::deadline::check(deadline)
}
pub fn members(&self, deadline: Instant) -> Result<Vec<u32>> {
self.check_limits(deadline)?;
let mut storage = vec![0usize; 2 + MAX_MEMBERS];
let bytes = std::mem::size_of_val(storage.as_slice());
let mut returned = 0u32;
unsafe {
QueryInformationJobObject(
Some(self.handle.0),
JobObjectBasicProcessIdList,
storage.as_mut_ptr().cast(),
bytes as u32,
Some(&mut returned),
)
}
.map_err(|_| unavailable())?;
let (assigned, count) = unsafe {
let header = storage.as_ptr().cast::<u32>();
(*header, *header.add(1) as usize)
};
let offset = std::mem::offset_of!(JOBOBJECT_BASIC_PROCESS_ID_LIST, ProcessIdList);
if count > MAX_MEMBERS
|| assigned as usize != count
|| returned as usize > bytes
|| (returned as usize) < offset + count * std::mem::size_of::<usize>()
|| offset + count * std::mem::size_of::<usize>() > bytes
{
return Err(unavailable());
}
let mut result = Vec::with_capacity(count);
for index in 0..count {
let pid = unsafe {
*storage
.as_ptr()
.cast::<u8>()
.add(offset)
.cast::<usize>()
.add(index)
};
let pid = u32::try_from(pid).map_err(|_| unavailable())?;
if pid == 0 || result.contains(&pid) {
return Err(unavailable());
}
result.push(pid);
}
crate::core::deadline::check(deadline)?;
Ok(result)
}
pub fn contains(&self, process: &Pinned, deadline: Instant) -> Result<bool> {
process.verify(deadline)?;
let found = self
.members(deadline)?
.contains(&process.identity().process_id());
process.verify(deadline)?;
Ok(found)
}
#[cfg(test)]
fn assign_test_child(&self, child: &Pinned) -> Result<()> {
unsafe { AssignProcessToJobObject(self.handle.0, HANDLE(child.native_handle().ptr())) }
.map_err(|_| unavailable())
}
}
fn unavailable() -> AppError {
AppError::new(
"JOB_UNVERIFIABLE",
"Private process job could not be verified; no process ownership was inferred.",
4,
)
}
#[cfg(test)]
mod tests;