blob: 2d7049e86270e015339fe47ca787f4545af48fca [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.
*/
#![cfg(windows)]
use maka_agent_facts::terminal::TerminalSize;
use maka_process::{Command, ProcessOutcome, pipe, pty};
use maka_sandbox::{
filesystem::Scope,
windows::{
WriteCapability, WriteToken,
acl::{self, Permission},
},
};
use std::{
fs::OpenOptions, os::windows::fs::OpenOptionsExt, path::PathBuf, sync::Arc, time::Duration,
};
use tokio::io::AsyncReadExt;
use tokio_util::sync::CancellationToken;
use windows_sys::Win32::Storage::FileSystem::{
FILE_FLAG_BACKUP_SEMANTICS, READ_CONTROL, WRITE_DAC,
};
// This exercises the token at the real native launch boundary. It deliberately
// does not claim to validate dedicated-account or network provisioning.
#[tokio::test]
async fn write_restriction_is_present_at_foreground_pipe_and_conpty_startup() {
tokio::time::timeout(Duration::from_secs(30), async {
let root = tempfile::tempdir().unwrap();
let work = root.path().join("work");
std::fs::create_dir(&work).unwrap();
std::fs::write(root.path().join("outside"), "unchanged").unwrap();
let capability = WriteCapability::new(uuid::Uuid::new_v4());
let directory = OpenOptions::new()
.access_mode(READ_CONTROL | WRITE_DAC)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(&work).unwrap();
acl::set(&directory, capability.sid(), Scope::Subtree, Some(Permission::Write)).unwrap();
let token = Arc::new(WriteToken::current(std::slice::from_ref(&capability)).unwrap());
let executable = PathBuf::from(std::env::var_os("SystemRoot").unwrap())
.join(r"System32\WindowsPowerShell\v1.0\powershell.exe");
let command = |terminal| {
let mut plan = Command::new(&executable, &work).with_write_token(token.clone());
// PowerShell performs its system-policy probe in TEMP. An unwritable
// inherited profile temp directory incorrectly forces constrained
// language mode. Give this execution its explicitly writable scratch.
plan.env("TEMP", &work).env("TMP", &work);
plan.args(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"]);
plan.arg(format!(r#"
$ErrorActionPreference='Stop'
if ([Console]::IsOutputRedirected -eq [bool]::Parse('{terminal}')) {{ exit 71 }}
[IO.File]::WriteAllText((Join-Path $pwd 'allowed'), 'written')
try {{ [IO.File]::WriteAllText((Join-Path $pwd '..\outside'), 'escaped'); exit 72 }}
catch {{ if (($_.Exception.GetBaseException().HResult -band 65535) -ne 5) {{ exit 73 }} }}
$child = Start-Process -FilePath $env:ComSpec -ArgumentList '/d /c "echo escaped > ..\outside"' -NoNewWindow -PassThru -Wait
if ($child.ExitCode -eq 0) {{ exit 74 }}
[Console]::WriteLine('token-io')
exit 17
"#));
plan
};
let observed = command(false).observe(Some(10_000), CancellationToken::new()).unwrap();
let captured = observed.completion.await.unwrap();
assert!(matches!(captured.outcome, ProcessOutcome::Exited(exit) if exit.code() == Some(17)), "{captured:?}");
assert!(captured.stdout.contains("token-io"), "{captured:?}");
let mut child = pipe::spawn(command(false)).await.unwrap();
drop(child.stdin);
let (mut out, mut err) = (String::new(), Vec::new());
let (status, output, errors) = tokio::join!(child.child.wait(), child.stdout.read_to_string(&mut out), child.stderr.read_to_end(&mut err));
output.unwrap();
errors.unwrap();
assert_eq!(status.unwrap().code(), Some(17), "{out}\n{err:?}");
assert!(out.contains("token-io"));
let (mut child, io) = pty::spawn(command(true), TerminalSize::new(80, 24).unwrap()).await.unwrap();
let (status, output) = tokio::join!(async {
let status = child.wait().await.unwrap();
child.close().await.unwrap();
status
}, async {
let mut output = Vec::new();
let mut buffer = [0; 4096];
loop {
let count = io.read(&mut buffer).await.unwrap();
if count == 0 { break; }
output.extend_from_slice(&buffer[..count]);
assert!(output.len() < 65536);
}
String::from_utf8(output).unwrap()
});
assert_eq!(status.code(), Some(17), "{output}");
assert!(output.contains("token-io"));
assert_eq!(std::fs::read_to_string(root.path().join("outside")).unwrap(), "unchanged");
assert_eq!(std::fs::read_to_string(work.join("allowed")).unwrap(), "written");
acl::set(&directory, capability.sid(), Scope::Subtree, None).unwrap();
}).await.expect("restricted native I/O and cleanup must be bounded");
}