Files
NotesAgentic/frontend/src-tauri/src/extension_launch_data.rs
T

295 lines
10 KiB
Rust

//! 本机 ​​argv/环境编码。这不会授权或启动进程。
use crate::workspace::{HostError, Result};
use std::{
collections::BTreeMap,
os::windows::ffi::OsStrExt,
path::{Path, PathBuf},
};
use zeroize::Zeroize;
pub struct LaunchData {
command: Vec<u16>,
environment: Vec<u16>,
scratch: PathBuf,
}
impl Drop for LaunchData {
fn drop(&mut self) {
self.command.zeroize();
self.environment.zeroize();
}
}
impl LaunchData {
/// Host 提供经过验证的绝对路径和显式声明/解析的环境值。从不读取父环境。 CRT argv 规则适用于本机可执行文件,而不是 cmd.exe、批处理文件或 shell 解释器。
pub fn new(
executable: &Path,
arguments: &[String],
system_root: &Path,
local_app_data: &Path,
scratch: &Path,
declared: &BTreeMap<String, String>,
) -> Result<Self> {
let bad = || HostError::new("EXTENSION_LAUNCH_DATA_INVALID");
if !executable.is_absolute()
|| !executable
.extension()
.is_some_and(|v| v.eq_ignore_ascii_case("exe"))
|| arguments.len() > 128
|| declared.len() > 128
{
return Err(bad());
}
let executable: Vec<u16> = executable.as_os_str().encode_wide().collect();
if executable.contains(&0) || executable.contains(&34) || executable.len() > 32764 {
return Err(bad());
}
let mut result = Self {
command: Vec::with_capacity(32767),
environment: Vec::with_capacity(32767),
scratch: scratch.to_owned(),
};
result.command.push(34);
result.command.extend(executable);
result.command.push(34);
for argument in arguments {
if argument.len() > 8192 || argument.contains('\0') {
return Err(bad());
}
// 保留一次完整的有界缓冲区:不要通过 Vec 重新分配留下解析值的早期副本。
let mut encoded_len = 0;
let mut trailing = 0;
for unit in argument.encode_utf16() {
if unit == 92 {
trailing += 1;
} else {
encoded_len += if unit == 34 {
trailing * 2 + 2
} else {
trailing + 1
};
trailing = 0;
}
}
encoded_len += trailing * 2;
if result.command.len() + 3 + encoded_len + 1 > 32767 {
return Err(bad());
}
result.command.extend([32, 34]);
let mut slashes = 0;
for unit in argument.encode_utf16() {
if unit == 92 {
slashes += 1;
continue;
}
result.command.extend(std::iter::repeat_n(
92,
if unit == 34 { slashes * 2 + 1 } else { slashes },
));
result.command.push(unit);
slashes = 0;
}
result.command.extend(std::iter::repeat_n(92, slashes * 2));
result.command.push(34);
if result.command.len() >= 32767 {
return Err(bad());
}
}
result.command.push(0);
if result.command.len() > 32767 {
return Err(bad());
}
// AppContainer 会把用户 LocalAppData 下的逻辑路径重定向到配置文件的 AC 目录。
// 直接把物理 AC 路径交给子进程会被再次重定向,形成 AC\Packages\...\AC 的错误路径。
let (visible_local_app_data, visible_scratch) =
visible_container_paths(local_app_data, scratch);
// ASCII 名称给出确定性的 Windows 不区分大小写的顺序。值在编码之前一直是借用的,因此不存在秘密克隆。
let mut fields: BTreeMap<String, &std::ffi::OsStr> = BTreeMap::new();
for (name, path) in [
("SYSTEMROOT", system_root),
("LOCALAPPDATA", visible_local_app_data.as_path()),
("TEMP", visible_scratch.as_path()),
("TMP", visible_scratch.as_path()),
] {
if !path.is_absolute() {
return Err(bad());
}
fields.insert(name.to_owned(), path.as_os_str());
}
for (name, value) in declared {
if name.is_empty()
|| name.len() > 128
|| !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
|| value.len() > 8192
{
return Err(bad());
}
if fields
.insert(name.to_ascii_uppercase(), value.as_ref())
.is_some()
{
return Err(bad());
}
}
for (name, value) in fields {
if result.environment.len() + name.len() + value.encode_wide().count() + 3 > 32767 {
return Err(bad());
}
result.environment.extend(name.encode_utf16());
result.environment.push(61);
for unit in value.encode_wide() {
if unit == 0 {
return Err(bad());
}
result.environment.push(unit);
}
result.environment.push(0);
if result.environment.len() >= 32767 {
return Err(bad());
}
}
result.environment.push(0);
Ok(result)
}
pub(crate) fn command_mut(&mut self) -> &mut [u16] {
&mut self.command
}
/// 通过CREATE_UNICODE_ENVIRONMENT;切勿替换空指针。
pub fn environment(&self) -> &[u16] {
&self.environment
}
pub(crate) fn scratch(&self) -> &Path {
&self.scratch
}
}
fn visible_container_paths(local_app_data: &Path, scratch: &Path) -> (PathBuf, PathBuf) {
let is_ac = local_app_data
.file_name()
.is_some_and(|name| name.eq_ignore_ascii_case("AC"));
let is_package = local_app_data
.parent()
.and_then(Path::parent)
.and_then(Path::file_name)
.is_some_and(|name| name.eq_ignore_ascii_case("Packages"));
if is_ac && is_package {
if let (Some(base), Ok(relative)) = (
local_app_data.ancestors().nth(3),
scratch.strip_prefix(local_app_data),
) {
return (base.to_owned(), base.join(relative));
}
}
(local_app_data.to_owned(), scratch.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
fn build(args: &[String], vars: &BTreeMap<String, String>) -> Result<LaunchData> {
LaunchData::new(
Path::new(r"C:\package\probe.exe"),
args,
Path::new(r"C:\Windows"),
Path::new(r"C:\container"),
Path::new(r"C:\scratch"),
vars,
)
}
#[test]
fn rejects_ambiguous_environment_names_reserved_overrides_nul_and_limits() {
for fields in [
vec![("temp", "x")],
vec![("a", "1"), ("A", "2")],
vec![("=C:", "x")],
vec![("SAFE", "x\0y")],
] {
assert!(build(
&[],
&fields
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect()
)
.is_err());
}
assert!(build(&["x\0y".into()], &BTreeMap::new()).is_err());
assert!(build(&vec!["x".repeat(8192); 4], &BTreeMap::new()).is_err());
assert!(build(
&[],
&(0..4)
.map(|i| (format!("V{i}"), "x".repeat(8192)))
.collect()
)
.is_err());
assert!(build(&vec![String::new(); 129], &BTreeMap::new()).is_err());
}
#[test]
fn refuses_relative_or_non_native_entry_and_preserves_wide_paths() {
for entry in [
r"relative.exe",
r"C:\package\entry.cmd",
"C:\\bad\"name.exe",
] {
assert!(LaunchData::new(
Path::new(entry),
&[],
Path::new(r"C:\Windows"),
Path::new(r"C:\container"),
Path::new(r"C:\scratch"),
&BTreeMap::new()
)
.is_err());
}
use std::os::windows::ffi::OsStringExt;
let wide = std::ffi::OsString::from_wide(&[67, 58, 92, 0xd800]);
let data = LaunchData::new(
Path::new(r"C:\probe.exe"),
&[],
Path::new(r"C:\Windows"),
Path::new(&wide),
Path::new(&wide),
&BTreeMap::new(),
)
.unwrap();
assert!(data.environment().contains(&0xd800));
assert!(data.environment().ends_with(&[0, 0]));
}
#[test]
fn converts_physical_appcontainer_paths_to_child_visible_paths() {
let (local, scratch) = visible_container_paths(
Path::new(r"C:\Users\tester\AppData\Local\Packages\OpenNexus.sandbox.id\AC"),
Path::new(r"C:\Users\tester\AppData\Local\Packages\OpenNexus.sandbox.id\AC\Temp"),
);
assert_eq!(local, Path::new(r"C:\Users\tester\AppData\Local"));
assert_eq!(scratch, Path::new(r"C:\Users\tester\AppData\Local\Temp"));
}
#[test]
fn shell_arguments_and_environment_injection_pass_hundred_round_matrix() {
let arguments = (0..100)
.map(|index| format!(r#"attack-{index} & | < > ^ %COMSPEC% $(echo injected) \" \\"#))
.collect::<Vec<_>>();
let mut launch = build(&arguments, &BTreeMap::new()).unwrap();
let encoded = launch.command_mut();
assert!(encoded
.windows(9)
.any(|value| value == "attack-99".encode_utf16().collect::<Vec<_>>()));
for index in 0..100 {
let name = match index % 5 {
0 => "TEMP".to_owned(),
1 => "tmp".to_owned(),
2 => "SystemRoot".to_owned(),
3 => "LOCALAPPDATA".to_owned(),
_ => format!("BAD={index}"),
};
assert_eq!(
build(&[], &BTreeMap::from([(name, "injected".into())]))
.err()
.unwrap()
.code,
"EXTENSION_LAUNCH_DATA_INVALID"
);
}
}
}