feat(sandbox): 通过执行租约撤销活动实例
This commit is contained in:
@@ -419,3 +419,13 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写
|
|||||||
- 实际 Stronghold 测试验证 Provider 同名凭据不可见、正确 MCP 包域可用、重新签发后改变类型/来源/命名空间/包 ID 仍不能读取原凭据,以及入口/树/Vault/策略不匹配、参数篡改、过期、锁定和编码错误拒绝。真实 AppContainer 原生探针通过该准备链取得虚构测试凭据后启动,完整 argv/环境核对通过,未使用任何用户真实秘密。
|
- 实际 Stronghold 测试验证 Provider 同名凭据不可见、正确 MCP 包域可用、重新签发后改变类型/来源/命名空间/包 ID 仍不能读取原凭据,以及入口/树/Vault/策略不匹配、参数篡改、过期、锁定和编码错误拒绝。真实 AppContainer 原生探针通过该准备链取得虚构测试凭据后启动,完整 argv/环境核对通过,未使用任何用户真实秘密。
|
||||||
- 52 项扩展回归通过,4 项 ignored 为三个由父测试驱动的 Job 辅助入口和单独执行的 60 秒验收;全目标 Clippy -D warnings 通过。日志 `.build/extension-launch-authorization-tests.log`、`.build/extension-launch-authorization-clippy.log`。
|
- 52 项扩展回归通过,4 项 ignored 为三个由父测试驱动的 Job 辅助入口和单独执行的 60 秒验收;全目标 Clippy -D warnings 通过。日志 `.build/extension-launch-authorization-tests.log`、`.build/extension-launch-authorization-clippy.log`。
|
||||||
- 该组件只完成准备阶段,不是可直接恢复执行的授权租约;调用方仍须在 resume 前复核在线信任、活动安装身份、许可、Vault 和锁定代次,并完成运行中撤销/停止、broker 和完整资源限制。目前未开放第三方执行,整体生产化继续进行。
|
- 该组件只完成准备阶段,不是可直接恢复执行的授权租约;调用方仍须在 resume 前复核在线信任、活动安装身份、许可、Vault 和锁定代次,并完成运行中撤销/停止、broker 和完整资源限制。目前未开放第三方执行,整体生产化继续进行。
|
||||||
|
|
||||||
|
|
||||||
|
## 增量:运行租约、锁定代次与主动进程树撤销
|
||||||
|
|
||||||
|
- 执行许可更新为内存态 v2 HMAC,包含签发代次;Authority.invalidate_all 与 Authority 释放都会推进代次。Lease 绑定原许可代次及由有效期计算的单调时钟期限,不能把撤销后的当前代次重新当作旧许可的有效代次。
|
||||||
|
- Context.prepare 在解析凭据前捕获租约与保险库锁定代次,生成不可拆换的 PreparedLaunch;创建挂起进程前再次核对租约、绑定路径、相对入口和树摘要,创建后继续核对。LeasedSuspended 恢复时先建立原生撤销监视器,进程生命周期继续保留入口与 Profile 借用。
|
||||||
|
- 监视器每 50 ms 检查许可代次、凭据代次与到期时间;任何失效均终止所属 Job,不要求调用者继续轮询。监视线程退出也有整组终止兜底;监视器不可用时不恢复执行。Running.start_tool_call 先检查租约,阻止已撤销实例启动新工具调用。
|
||||||
|
- 真实 AppContainer 双进程探针验证:本轮许可作废后约 56.90 ms、保险库锁定后约 47.80 ms、签发器释放后约 55.45 ms 观察到进程树清空;之后新工具调用均被拒绝。两秒测试租约自然到期也主动清空进程树。创建前撤销和创建后恢复前撤销均拒绝继续,无普通子进程降级。
|
||||||
|
- 原生参数/环境探针已使用 prepare → create_suspended → 带租约恢复路径。52 项扩展回归通过,4 项 ignored 为三个父测试实际驱动的 Job 辅助入口及单独执行的 60 秒验收;全目标 Clippy -D warnings 通过。日志 `.build/extension-revocation-tests.log`、`.build/extension-revocation-clippy.log`。
|
||||||
|
- 本轮锁定由真实 CredentialBroker.lock 触发,尚不是操作系统锁屏事件的桌面端到端测试。监视器提供有界撤销,并非对核验与 ResumeThread 之间竞态作“零指令执行”证明。Host 仍需把实际 Vault 切换、权限撤回、在线签名撤销等事件接入签发器/实例注册表,并完成 broker 与全部资源策略,整体生产化尚未完成。
|
||||||
|
|||||||
@@ -764,17 +764,98 @@ mod tests {
|
|||||||
container_data: &folder,
|
container_data: &folder,
|
||||||
scratch: &folder.join("Temp"),
|
scratch: &folder.join("Temp"),
|
||||||
};
|
};
|
||||||
let data = context
|
let prepared = context
|
||||||
.build(&authority, &permit, &claims, &bound_entry, &broker, 2)
|
.prepare(&authority, &permit, &claims, &bound_entry, &broker, 2)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let suspended =
|
let suspended = prepared.create_suspended(&profile, &bound_entry).unwrap();
|
||||||
crate::extension_process::Suspended::create_bound(&profile, &bound_entry, data)
|
|
||||||
.unwrap();
|
|
||||||
let running = unsafe { suspended.resume().unwrap() };
|
let running = unsafe { suspended.resume().unwrap() };
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
running.wait(std::time::Duration::from_secs(5)).unwrap(),
|
running.wait(std::time::Duration::from_secs(5)).unwrap(),
|
||||||
Some(0)
|
Some(0)
|
||||||
);
|
);
|
||||||
|
drop(running);
|
||||||
|
for cause in [
|
||||||
|
"before_create",
|
||||||
|
"before_resume",
|
||||||
|
"permit",
|
||||||
|
"credential",
|
||||||
|
"owner_drop",
|
||||||
|
"expiry",
|
||||||
|
] {
|
||||||
|
let mut issuer = Authority::default();
|
||||||
|
let mut waiting = claims.clone();
|
||||||
|
waiting.arguments = vec!["wait_tree".into()];
|
||||||
|
waiting.expires_at_ms = if cause == "expiry" { 2_002 } else { 120_000 };
|
||||||
|
let permit = issuer.issue(&waiting, 1).unwrap();
|
||||||
|
let prepared = context
|
||||||
|
.prepare(&issuer, &permit, &waiting, &bound_entry, &broker, 2)
|
||||||
|
.unwrap();
|
||||||
|
if cause == "before_create" {
|
||||||
|
issuer.invalidate_all();
|
||||||
|
assert_eq!(
|
||||||
|
prepared
|
||||||
|
.create_suspended(&profile, &bound_entry)
|
||||||
|
.err()
|
||||||
|
.unwrap()
|
||||||
|
.code,
|
||||||
|
"EXTENSION_PERMIT_REVOKED"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let suspended = prepared.create_suspended(&profile, &bound_entry).unwrap();
|
||||||
|
if cause == "before_resume" {
|
||||||
|
issuer.invalidate_all();
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { suspended.resume() }.err().unwrap().code,
|
||||||
|
"EXTENSION_PERMIT_REVOKED"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let running = unsafe { suspended.resume().unwrap() };
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
while running.active_test_processes().unwrap() != 2
|
||||||
|
&& started.elapsed() < std::time::Duration::from_secs(5)
|
||||||
|
{
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
assert_eq!(running.active_test_processes().unwrap(), 2);
|
||||||
|
let revoked = std::time::Instant::now();
|
||||||
|
match cause {
|
||||||
|
"permit" => issuer.invalidate_all(),
|
||||||
|
"credential" => broker.lock(),
|
||||||
|
"expiry" => {}
|
||||||
|
_ => drop(issuer),
|
||||||
|
}
|
||||||
|
// The monitor must act without check_authorization or tool polling.
|
||||||
|
assert!(running
|
||||||
|
.wait(std::time::Duration::from_secs(5))
|
||||||
|
.unwrap()
|
||||||
|
.is_some());
|
||||||
|
while running.active_test_processes().unwrap() != 0
|
||||||
|
&& revoked.elapsed() < std::time::Duration::from_secs(5)
|
||||||
|
{
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
assert_eq!(running.active_test_processes().unwrap(), 0);
|
||||||
|
assert!(revoked.elapsed() < std::time::Duration::from_secs(5));
|
||||||
|
let expected = match cause {
|
||||||
|
"credential" => "CREDENTIALS_LOCKED",
|
||||||
|
"expiry" => "EXTENSION_PERMIT_EXPIRED",
|
||||||
|
_ => "EXTENSION_PERMIT_REVOKED",
|
||||||
|
};
|
||||||
|
assert_eq!(running.check_authorization().unwrap_err().code, expected);
|
||||||
|
assert_eq!(running.start_tool_call().err().unwrap().code, expected);
|
||||||
|
eprintln!(
|
||||||
|
"instance revocation {cause}: tree empty after {:?}",
|
||||||
|
revoked.elapsed()
|
||||||
|
);
|
||||||
|
drop(running);
|
||||||
|
if cause == "credential" {
|
||||||
|
broker
|
||||||
|
.unlock(Zeroizing::new(b"native fixture passphrase".to_vec()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "desktop"))]
|
#[cfg(not(feature = "desktop"))]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -19,6 +19,46 @@ pub struct Context<'a> {
|
|||||||
pub container_data: &'a Path,
|
pub container_data: &'a Path,
|
||||||
pub scratch: &'a Path,
|
pub scratch: &'a Path,
|
||||||
}
|
}
|
||||||
|
pub struct PreparedLaunch {
|
||||||
|
data: LaunchData,
|
||||||
|
lease: crate::extension_permit::Lease,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
entry: String,
|
||||||
|
tree: String,
|
||||||
|
}
|
||||||
|
pub struct LeasedSuspended<'a> {
|
||||||
|
process: crate::extension_process::Suspended<'a>,
|
||||||
|
lease: crate::extension_permit::Lease,
|
||||||
|
}
|
||||||
|
impl PreparedLaunch {
|
||||||
|
pub fn create_suspended<'a>(
|
||||||
|
self,
|
||||||
|
profile: &'a crate::extension_container::Profile,
|
||||||
|
entry: &'a BoundEntry<'a>,
|
||||||
|
) -> Result<LeasedSuspended<'a>> {
|
||||||
|
self.lease.check()?;
|
||||||
|
if self.path != entry.path()
|
||||||
|
|| self.entry != entry.relative_name()
|
||||||
|
|| self.tree != entry.tree_sha256()
|
||||||
|
{
|
||||||
|
return Err(HostError::new("EXTENSION_ENTRY_PERMIT_MISMATCH"));
|
||||||
|
}
|
||||||
|
let process = crate::extension_process::Suspended::create_bound(profile, entry, self.data)?;
|
||||||
|
self.lease.check()?;
|
||||||
|
Ok(LeasedSuspended {
|
||||||
|
process,
|
||||||
|
lease: self.lease,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> LeasedSuspended<'a> {
|
||||||
|
/// # Safety
|
||||||
|
/// Live trust, active installation, broker and all sandbox resource policy
|
||||||
|
/// requirements must also hold. A lease does not establish those conditions.
|
||||||
|
pub unsafe fn resume(self) -> Result<crate::extension_process::Running<'a>> {
|
||||||
|
unsafe { self.process.resume_with_lease(self.lease) }
|
||||||
|
}
|
||||||
|
}
|
||||||
struct EnvironmentValues(BTreeMap<String, String>);
|
struct EnvironmentValues(BTreeMap<String, String>);
|
||||||
impl Drop for EnvironmentValues {
|
impl Drop for EnvironmentValues {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
@@ -66,6 +106,35 @@ pub fn credential_id(claims: &Claims, reference: &str) -> Result<CredentialId> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
impl Context<'_> {
|
impl Context<'_> {
|
||||||
|
/// Capture epochs before resolving credentials; never adopt a newer lock
|
||||||
|
/// generation for launch bytes prepared under an earlier session.
|
||||||
|
pub fn prepare(
|
||||||
|
&self,
|
||||||
|
authority: &Authority,
|
||||||
|
permit: &Permit,
|
||||||
|
claims: &Claims,
|
||||||
|
entry: &BoundEntry<'_>,
|
||||||
|
broker: &CredentialBroker,
|
||||||
|
now_ms: u64,
|
||||||
|
) -> Result<PreparedLaunch> {
|
||||||
|
let mut lease = authority.lease(permit, claims, now_ms)?;
|
||||||
|
if claims
|
||||||
|
.environment
|
||||||
|
.values()
|
||||||
|
.any(|v| matches!(v, Environment::CredentialScope(_)))
|
||||||
|
{
|
||||||
|
lease.bind_credential(broker.lock_signal());
|
||||||
|
}
|
||||||
|
let data = self.build(authority, permit, claims, entry, broker, now_ms)?;
|
||||||
|
lease.check()?;
|
||||||
|
Ok(PreparedLaunch {
|
||||||
|
data,
|
||||||
|
lease,
|
||||||
|
path: entry.path().to_owned(),
|
||||||
|
entry: entry.relative_name().to_owned(),
|
||||||
|
tree: entry.tree_sha256().to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
/// Caller must still recheck live trust/permit/session state immediately
|
/// Caller must still recheck live trust/permit/session state immediately
|
||||||
/// before resume; returning encoded data is not an execution lease.
|
/// before resume; returning encoded data is not an execution lease.
|
||||||
pub fn build(
|
pub fn build(
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ use rand::RngCore;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::{
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicU64, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
@@ -46,12 +53,46 @@ pub struct Claims {
|
|||||||
|
|
||||||
/// Opaque authenticator; the Host retains claims separately. No paths, arguments
|
/// Opaque authenticator; the Host retains claims separately. No paths, arguments
|
||||||
/// or credential declarations need to be passed to a renderer with the token.
|
/// or credential declarations need to be passed to a renderer with the token.
|
||||||
pub struct Permit([u8; 32]);
|
pub struct Permit {
|
||||||
|
mac: [u8; 32],
|
||||||
|
generation: u64,
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Lease {
|
||||||
|
generation: (Arc<AtomicU64>, u64),
|
||||||
|
credential: Option<(Arc<AtomicU64>, u64)>,
|
||||||
|
expires: Instant,
|
||||||
|
}
|
||||||
|
impl Lease {
|
||||||
|
pub fn check(&self) -> Result<()> {
|
||||||
|
if self.generation.0.load(Ordering::SeqCst) != self.generation.1 {
|
||||||
|
return Err(HostError::new("EXTENSION_PERMIT_REVOKED"));
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.credential
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|(signal, epoch)| signal.load(Ordering::SeqCst) != *epoch)
|
||||||
|
{
|
||||||
|
return Err(HostError::new("CREDENTIALS_LOCKED"));
|
||||||
|
}
|
||||||
|
if Instant::now() >= self.expires {
|
||||||
|
return Err(HostError::new("EXTENSION_PERMIT_EXPIRED"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub(crate) fn bind_credential(&mut self, signal: Arc<AtomicU64>) {
|
||||||
|
let epoch = signal.load(Ordering::SeqCst);
|
||||||
|
self.credential = Some((signal, epoch));
|
||||||
|
}
|
||||||
|
}
|
||||||
pub struct Authority {
|
pub struct Authority {
|
||||||
key: [u8; 32],
|
key: [u8; 32],
|
||||||
|
generation: Arc<AtomicU64>,
|
||||||
}
|
}
|
||||||
impl Drop for Authority {
|
impl Drop for Authority {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
self.generation.fetch_add(1, Ordering::SeqCst);
|
||||||
self.key.zeroize();
|
self.key.zeroize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +100,10 @@ impl Default for Authority {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut key = [0; 32];
|
let mut key = [0; 32];
|
||||||
rand::rngs::OsRng.fill_bytes(&mut key);
|
rand::rngs::OsRng.fill_bytes(&mut key);
|
||||||
Self { key }
|
Self {
|
||||||
|
key,
|
||||||
|
generation: Arc::new(AtomicU64::new(0)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Claims {
|
impl Claims {
|
||||||
@@ -157,23 +201,52 @@ impl Authority {
|
|||||||
/// Call only after consent and live trust validation. This authenticates the
|
/// Call only after consent and live trust validation. This authenticates the
|
||||||
/// decision; it does not establish sandbox availability or grant broker access.
|
/// decision; it does not establish sandbox availability or grant broker access.
|
||||||
pub fn issue(&self, claims: &Claims, now_ms: u64) -> Result<Permit> {
|
pub fn issue(&self, claims: &Claims, now_ms: u64) -> Result<Permit> {
|
||||||
|
let generation = self.generation.load(Ordering::SeqCst);
|
||||||
let encoded = claims.encoded(now_ms)?;
|
let encoded = claims.encoded(now_ms)?;
|
||||||
let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("HMAC key");
|
let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("HMAC key");
|
||||||
mac.update(b"OpenNexus execution permit v1\0");
|
mac.update(b"OpenNexus execution permit v2\0");
|
||||||
|
mac.update(&generation.to_be_bytes());
|
||||||
mac.update(&encoded);
|
mac.update(&encoded);
|
||||||
Ok(Permit(mac.finalize().into_bytes().into()))
|
Ok(Permit {
|
||||||
|
mac: mac.finalize().into_bytes().into(),
|
||||||
|
generation,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
pub fn verify(&self, permit: &Permit, actual: &Claims, now_ms: u64) -> Result<()> {
|
pub fn verify(&self, permit: &Permit, actual: &Claims, now_ms: u64) -> Result<()> {
|
||||||
|
let generation = self.generation.load(Ordering::SeqCst);
|
||||||
|
if generation != permit.generation {
|
||||||
|
return Err(HostError::new("EXTENSION_PERMIT_REVOKED"));
|
||||||
|
}
|
||||||
let encoded = actual.encoded(now_ms)?;
|
let encoded = actual.encoded(now_ms)?;
|
||||||
let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("HMAC key");
|
let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("HMAC key");
|
||||||
mac.update(b"OpenNexus execution permit v1\0");
|
mac.update(b"OpenNexus execution permit v2\0");
|
||||||
|
mac.update(&generation.to_be_bytes());
|
||||||
mac.update(&encoded);
|
mac.update(&encoded);
|
||||||
mac.verify_slice(&permit.0)
|
mac.verify_slice(&permit.mac)
|
||||||
.map_err(|_| HostError::new("EXTENSION_PERMIT_MISMATCH"))
|
.map_err(|_| HostError::new("EXTENSION_PERMIT_MISMATCH"))?;
|
||||||
|
if generation != self.generation.load(Ordering::SeqCst) {
|
||||||
|
return Err(HostError::new("EXTENSION_PERMIT_REVOKED"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn lease(&self, permit: &Permit, claims: &Claims, now_ms: u64) -> Result<Lease> {
|
||||||
|
let started = Instant::now();
|
||||||
|
self.verify(permit, claims, now_ms)?;
|
||||||
|
let expires = started
|
||||||
|
.checked_add(Duration::from_millis(claims.expires_at_ms - now_ms))
|
||||||
|
.ok_or_else(|| HostError::new("EXTENSION_PERMIT_INVALID"))?;
|
||||||
|
let lease = Lease {
|
||||||
|
generation: (Arc::clone(&self.generation), permit.generation),
|
||||||
|
credential: None,
|
||||||
|
expires,
|
||||||
|
};
|
||||||
|
lease.check()?;
|
||||||
|
Ok(lease)
|
||||||
}
|
}
|
||||||
/// Lock/logout/policy invalidation may discard all permits. Restart creates a
|
/// Lock/logout/policy invalidation may discard all permits. Restart creates a
|
||||||
/// fresh key, so an old process token cannot silently revive authorization.
|
/// fresh key, so an old process token cannot silently revive authorization.
|
||||||
pub fn invalidate_all(&mut self) {
|
pub fn invalidate_all(&mut self) {
|
||||||
|
self.generation.fetch_add(1, Ordering::SeqCst);
|
||||||
self.key.zeroize();
|
self.key.zeroize();
|
||||||
rand::rngs::OsRng.fill_bytes(&mut self.key);
|
rand::rngs::OsRng.fill_bytes(&mut self.key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,11 @@ impl Drop for Process<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub struct Suspended<'a>(Process<'a>);
|
pub struct Suspended<'a>(Process<'a>);
|
||||||
pub struct Running<'a>(Process<'a>);
|
pub struct Running<'a> {
|
||||||
|
process: Process<'a>,
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
revocation: Option<crate::extension_revocation::Watch>,
|
||||||
|
}
|
||||||
impl<'a> Suspended<'a> {
|
impl<'a> Suspended<'a> {
|
||||||
/// Creates hidden, with no inherited handles and an explicit environment and
|
/// Creates hidden, with no inherited handles and an explicit environment and
|
||||||
/// current directory. The profile borrow prevents cleanup while this owner
|
/// current directory. The profile borrow prevents cleanup while this owner
|
||||||
@@ -187,25 +191,57 @@ impl<'a> Suspended<'a> {
|
|||||||
if unsafe { ResumeThread(self.0.handles.thread.as_raw_handle()) } != 1 {
|
if unsafe { ResumeThread(self.0.handles.thread.as_raw_handle()) } != 1 {
|
||||||
return Err(HostError::new("EXTENSION_PROCESS_RESUME_FAILED"));
|
return Err(HostError::new("EXTENSION_PROCESS_RESUME_FAILED"));
|
||||||
}
|
}
|
||||||
Ok(Running(self.0))
|
Ok(Running {
|
||||||
|
process: self.0,
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
revocation: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// # Safety
|
||||||
|
/// The same complete resource/broker/trust preconditions as resume apply.
|
||||||
|
/// This additionally arms revocation monitoring before any instruction resumes.
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
pub unsafe fn resume_with_lease(
|
||||||
|
self,
|
||||||
|
lease: crate::extension_permit::Lease,
|
||||||
|
) -> Result<Running<'a>> {
|
||||||
|
let watch = crate::extension_revocation::Watch::arm(&self.0.job, lease)?;
|
||||||
|
watch.check()?;
|
||||||
|
if unsafe { ResumeThread(self.0.handles.thread.as_raw_handle()) } != 1 {
|
||||||
|
return Err(HostError::new("EXTENSION_PROCESS_RESUME_FAILED"));
|
||||||
|
}
|
||||||
|
let running = Running {
|
||||||
|
process: self.0,
|
||||||
|
revocation: Some(watch),
|
||||||
|
};
|
||||||
|
running.check_authorization()?;
|
||||||
|
Ok(running)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Running<'_> {
|
impl Running<'_> {
|
||||||
|
pub fn check_authorization(&self) -> Result<()> {
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
if let Some(watch) = &self.revocation {
|
||||||
|
return watch.check();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn active_test_processes(&self) -> Result<u32> {
|
pub(crate) fn active_test_processes(&self) -> Result<u32> {
|
||||||
self.0.job.active_processes()
|
self.process.job.active_processes()
|
||||||
}
|
}
|
||||||
/// Arm before dispatching a tool request; finish after receiving its result.
|
/// Arm before dispatching a tool request; finish after receiving its result.
|
||||||
/// Failure to arm must prevent dispatch. This does not time server lifetime.
|
/// Failure to arm must prevent dispatch. This does not time server lifetime.
|
||||||
pub fn start_tool_call(&self) -> Result<crate::extension_deadline::ToolDeadline> {
|
pub fn start_tool_call(&self) -> Result<crate::extension_deadline::ToolDeadline> {
|
||||||
crate::extension_deadline::ToolDeadline::arm(&self.0.job)
|
self.check_authorization()?;
|
||||||
|
crate::extension_deadline::ToolDeadline::arm(&self.process.job)
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn start_test_tool_call(
|
pub(crate) fn start_test_tool_call(
|
||||||
&self,
|
&self,
|
||||||
budget: Duration,
|
budget: Duration,
|
||||||
) -> Result<crate::extension_deadline::ToolDeadline> {
|
) -> Result<crate::extension_deadline::ToolDeadline> {
|
||||||
crate::extension_deadline::ToolDeadline::arm_test(&self.0.job, budget)
|
crate::extension_deadline::ToolDeadline::arm_test(&self.process.job, budget)
|
||||||
}
|
}
|
||||||
/// A bounded observation only. The runtime must enforce the tool deadline.
|
/// A bounded observation only. The runtime must enforce the tool deadline.
|
||||||
pub fn wait(&self, timeout: Duration) -> Result<Option<u32>> {
|
pub fn wait(&self, timeout: Duration) -> Result<Option<u32>> {
|
||||||
@@ -213,12 +249,15 @@ impl Running<'_> {
|
|||||||
.ok()
|
.ok()
|
||||||
.filter(|n| *n <= 60000)
|
.filter(|n| *n <= 60000)
|
||||||
.ok_or_else(|| HostError::new("EXTENSION_PROCESS_WAIT_INVALID"))?;
|
.ok_or_else(|| HostError::new("EXTENSION_PROCESS_WAIT_INVALID"))?;
|
||||||
match unsafe { WaitForSingleObject(self.0.handles.process.as_raw_handle(), milliseconds) } {
|
match unsafe {
|
||||||
|
WaitForSingleObject(self.process.handles.process.as_raw_handle(), milliseconds)
|
||||||
|
} {
|
||||||
WAIT_TIMEOUT => Ok(None),
|
WAIT_TIMEOUT => Ok(None),
|
||||||
WAIT_OBJECT_0 => {
|
WAIT_OBJECT_0 => {
|
||||||
let mut code = 0;
|
let mut code = 0;
|
||||||
if unsafe { GetExitCodeProcess(self.0.handles.process.as_raw_handle(), &mut code) }
|
if unsafe {
|
||||||
== 0
|
GetExitCodeProcess(self.process.handles.process.as_raw_handle(), &mut code)
|
||||||
|
} == 0
|
||||||
{
|
{
|
||||||
return Err(HostError::new("EXTENSION_PROCESS_QUERY_FAILED"));
|
return Err(HostError::new("EXTENSION_PROCESS_QUERY_FAILED"));
|
||||||
}
|
}
|
||||||
@@ -229,7 +268,7 @@ impl Running<'_> {
|
|||||||
}
|
}
|
||||||
/// Terminates the entire managed group, including descendants.
|
/// Terminates the entire managed group, including descendants.
|
||||||
pub fn terminate(&self) -> Result<()> {
|
pub fn terminate(&self) -> Result<()> {
|
||||||
self.0.job.terminate()
|
self.process.job.terminate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn verify_identity(handles: &Handles, profile: &Profile) -> Result<()> {
|
fn verify_identity(handles: &Handles, profile: &Profile) -> Result<()> {
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
//! Native instance monitor; revocation does not depend on the caller polling.
|
||||||
|
use crate::{
|
||||||
|
extension_job::Job,
|
||||||
|
extension_permit::Lease,
|
||||||
|
workspace::{HostError, Result},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc, Condvar, Mutex,
|
||||||
|
},
|
||||||
|
thread::JoinHandle,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
struct KillOnExit(Job);
|
||||||
|
impl Drop for KillOnExit {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.0.terminate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) struct Watch {
|
||||||
|
lease: Lease,
|
||||||
|
cancelled: Arc<(Mutex<bool>, Condvar)>,
|
||||||
|
failed: Arc<AtomicBool>,
|
||||||
|
worker: Option<JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
impl Watch {
|
||||||
|
pub(crate) fn arm(job: &Job, lease: Lease) -> Result<Self> {
|
||||||
|
lease.check()?;
|
||||||
|
let job = KillOnExit(job.clone_for_deadline()?);
|
||||||
|
let cancelled = Arc::new((Mutex::new(false), Condvar::new()));
|
||||||
|
let failed = Arc::new(AtomicBool::new(false));
|
||||||
|
let thread_cancel = Arc::clone(&cancelled);
|
||||||
|
let thread_failed = Arc::clone(&failed);
|
||||||
|
let thread_lease = lease.clone();
|
||||||
|
let worker = std::thread::Builder::new()
|
||||||
|
.name("extension-revocation".into())
|
||||||
|
.spawn(move || {
|
||||||
|
let mut stop = thread_cancel.0.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
while !*stop {
|
||||||
|
if thread_lease.check().is_err() {
|
||||||
|
if job.0.terminate().is_err() {
|
||||||
|
thread_failed.store(true, Ordering::Release);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stop = thread_cancel
|
||||||
|
.1
|
||||||
|
.wait_timeout(stop, Duration::from_millis(50))
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|_| HostError::new("EXTENSION_REVOCATION_WATCH_UNAVAILABLE"))?;
|
||||||
|
Ok(Self {
|
||||||
|
lease,
|
||||||
|
cancelled,
|
||||||
|
failed,
|
||||||
|
worker: Some(worker),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub(crate) fn check(&self) -> Result<()> {
|
||||||
|
if self.failed.load(Ordering::Acquire) {
|
||||||
|
return Err(HostError::new("EXTENSION_RESOURCE_TERMINATE_FAILED"));
|
||||||
|
}
|
||||||
|
self.lease.check()?;
|
||||||
|
if self
|
||||||
|
.worker
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|worker| worker.is_finished())
|
||||||
|
{
|
||||||
|
return Err(HostError::new("EXTENSION_REVOCATION_WATCH_FAILED"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for Watch {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
*self.cancelled.0.lock().unwrap_or_else(|e| e.into_inner()) = true;
|
||||||
|
self.cancelled.1.notify_all();
|
||||||
|
if let Some(worker) = self.worker.take() {
|
||||||
|
let _ = worker.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,3 +72,6 @@ pub mod extension_pinned;
|
|||||||
|
|
||||||
#[cfg(all(windows, feature = "desktop"))]
|
#[cfg(all(windows, feature = "desktop"))]
|
||||||
pub mod extension_launch_authorization;
|
pub mod extension_launch_authorization;
|
||||||
|
|
||||||
|
#[cfg(all(windows, feature = "desktop"))]
|
||||||
|
mod extension_revocation;
|
||||||
|
|||||||
Reference in New Issue
Block a user