让 Linux npm argv 全程保留 OsString 不再经 lossy 往返
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m45s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m59s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m53s
Project CI / Frontend tests (pull_request) Successful in 4m13s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m48s
Project CI / Repository checks (pull_request) Successful in 6m39s
Project CI / Backend tests (pull_request) Successful in 10m3s
Project CI / Native shell tests (pull_request) Successful in 9m57s

- project_command_actual_target 返回 Vec<OsString>,npm_cli 以 as_os_str 原样放入 argv[0],非 UTF-8 路径不再被 U+FFFD 改写
- prepare_command_sandbox_launch / prepare_linux_command_sandbox_launch / LinuxSandboxPlan 的 arguments 改为 OsString,bwrap 直接透传
- command_sandbox_requests_npm_install 按 OsStr 逐字节比对可信 npm_cli,避免误拒合法的 npm install 联网
- process_session_bridge 直接对 OsString 取字节,去掉一次 String 中转
- 用例补齐非 UTF-8 的 npm_cli 必须命中;真机测试同步改用 OsString
This commit is contained in:
2026-10-07 14:26:54 +08:00
parent d706b65643
commit c31b214444
3 changed files with 78 additions and 46 deletions
@@ -961,19 +961,24 @@ fn resolve_project_command_executable(
}
/// Linux 上 npm 以 `node <npm-cli.js> <args>` 启动;其余情况保持 `executable + args`。
pub(crate) fn project_command_actual_target(spec: &ProjectCommandSpec) -> (PathBuf, Vec<String>) {
pub(crate) fn project_command_actual_target(spec: &ProjectCommandSpec) -> (PathBuf, Vec<OsString>) {
#[cfg(target_os = "linux")]
{
if let Some((node, npm_cli)) = spec.node_launcher.as_ref() {
let mut arguments = Vec::with_capacity(spec.arguments.len() + 1);
arguments.push(npm_cli.to_string_lossy().into_owned());
arguments.extend(spec.arguments.iter().cloned());
// 保留原始 OsStr 字节:`npm_cli` 若含非 UTF-8,`to_string_lossy` 会变成 U+FFFD,
// 既改坏实际执行的脚本路径,也让沙箱的可信 npm_cli 比对失配。
arguments.push(npm_cli.as_os_str().to_owned());
arguments.extend(spec.arguments.iter().map(OsString::from));
return (node.clone(), arguments);
}
}
(
spec.executable.clone(),
project_command_actual_arguments(spec),
project_command_actual_arguments(spec)
.into_iter()
.map(OsString::from)
.collect(),
)
}
@@ -2685,8 +2690,14 @@ mod tests {
Some("node"),
"{executable:?}"
);
assert!(arguments[0].ends_with("npm-cli.js"), "{arguments:?}");
assert_eq!(&arguments[1..], ["run".to_string(), "build".to_string()]);
assert!(
arguments[0].to_string_lossy().ends_with("npm-cli.js"),
"{arguments:?}"
);
assert_eq!(
&arguments[1..],
[OsString::from("run"), OsString::from("build")]
);
// 沙箱联网判定要求目标与解析层给出的可信 (node, npm_cli) 精确一致。
let (trusted_node, trusted_npm_cli) = spec
@@ -2694,15 +2705,16 @@ mod tests {
.as_ref()
.expect("Linux npm spec must carry the trusted node launcher");
assert_eq!(trusted_node, &executable);
assert_eq!(
trusted_npm_cli.to_string_lossy().as_ref(),
arguments[0].as_str()
);
// 逐字节相等:argv[0] 不再经 to_string_lossy 往返。
assert_eq!(trusted_npm_cli.as_os_str(), arguments[0].as_os_str());
let bootstrap = resolve_project_bootstrap_spec_at(root.path(), 30).unwrap();
let (_, arguments) = project_command_actual_target(&bootstrap);
assert!(arguments[0].ends_with("npm-cli.js"), "{arguments:?}");
assert_eq!(arguments[1], "install");
assert!(
arguments[0].to_string_lossy().ends_with("npm-cli.js"),
"{arguments:?}"
);
assert_eq!(arguments[1], OsString::from("install"));
}
#[cfg(target_os = "linux")]
@@ -117,7 +117,7 @@ pub(crate) fn command_sandbox_platform_metadata() -> CommandSandboxMetadata {
pub(crate) fn prepare_command_sandbox_launch(
root: &Path,
executable: &Path,
arguments: &[String],
arguments: &[OsString],
cwd: &Path,
environment: &[(OsString, OsString)],
trusted_npm_install: Option<(&Path, &Path)>,
@@ -212,7 +212,7 @@ mod linux {
root: PathBuf,
cwd: PathBuf,
executable: PathBuf,
arguments: Vec<String>,
arguments: Vec<OsString>,
npm_install_network: bool,
target_environment: Vec<(OsString, OsString)>,
merged_usr_links: Vec<(OsString, PathBuf)>,
@@ -302,7 +302,7 @@ mod linux {
pub(super) fn prepare_linux_command_sandbox_launch(
root: &Path,
executable: &Path,
arguments: &[String],
arguments: &[OsString],
cwd: &Path,
environment: &[(OsString, OsString)],
trusted_npm_install: Option<(&Path, &Path)>,
@@ -567,7 +567,7 @@ mod linux {
/// 给出的可信 `(node, npm_cli)` 精确比对;拿不到可信对时一律不放网。
fn command_sandbox_requests_npm_install(
executable: &Path,
arguments: &[String],
arguments: &[OsString],
trusted_npm_install: Option<(&Path, &Path)>,
) -> bool {
let Some((trusted_node, trusted_npm_cli)) = trusted_npm_install else {
@@ -579,7 +579,7 @@ mod linux {
.is_some_and(|argument| Path::new(argument) == trusted_npm_cli)
&& arguments
.get(1)
.is_some_and(|argument| argument == "install")
.is_some_and(|argument| argument.as_os_str() == OsStr::new("install"))
}
/// fnm / nvm / 系统 / 随包 Node 的安装前缀必须整棵只读挂进沙箱:只挂单个 `node` 或 `npm`
@@ -784,7 +784,7 @@ mod linux {
push_option(&mut args, "--chdir", [plan.cwd.as_os_str()]);
args.push(OsString::from("--"));
args.push(plan.executable.as_os_str().to_owned());
args.extend(plan.arguments.iter().map(OsString::from));
args.extend(plan.arguments.iter().cloned());
CommandSandboxLaunch {
executable: plan.bwrap,
@@ -1081,7 +1081,7 @@ mod linux {
root: PathBuf::from("/workspace/project"),
cwd: PathBuf::from("/workspace/project/game"),
executable: PathBuf::from("/opt/toolchain/bin/tool"),
arguments: vec!["check".to_string(), "--flag".to_string()],
arguments: vec![OsString::from("check"), OsString::from("--flag")],
npm_install_network: false,
target_environment: vec![
(
@@ -1204,7 +1204,7 @@ mod linux {
let error = prepare_command_sandbox_launch(
&root,
Path::new("/usr/bin/python3"),
&["-c".to_string(), script],
&[OsString::from("-c"), OsString::from(script)],
&root,
&[(OsString::from("PATH"), OsString::from("/usr/bin"))],
None,
@@ -1356,10 +1356,7 @@ mod linux {
let node = Path::new("/opt/node/bin/node");
let npm_cli = Path::new("/opt/node/lib/node_modules/npm/bin/npm-cli.js");
let trusted = Some((node, npm_cli));
let install_arguments = vec![
npm_cli.to_string_lossy().into_owned(),
"install".to_string(),
];
let install_arguments = vec![npm_cli.as_os_str().to_owned(), OsString::from("install")];
assert!(command_sandbox_requests_npm_install(
node,
&install_arguments,
@@ -1369,8 +1366,8 @@ mod linux {
assert!(!command_sandbox_requests_npm_install(
node,
&[
"/workspace/game/npm-cli.js".to_string(),
"install".to_string(),
OsString::from("/workspace/game/npm-cli.js"),
OsString::from("install"),
],
trusted,
));
@@ -1384,18 +1381,35 @@ mod linux {
assert!(!command_sandbox_requests_npm_install(
node,
&[
npm_cli.to_string_lossy().into_owned(),
"run".to_string(),
"build".to_string(),
npm_cli.as_os_str().to_owned(),
OsString::from("run"),
OsString::from("build"),
],
trusted,
));
// 没有可信 launcher(例如直接 npm shim)时不放网。
assert!(!command_sandbox_requests_npm_install(
Path::new("/usr/bin/npm"),
&["install".to_string()],
&[OsString::from("install")],
None,
));
// 非 UTF-8 的 npm_cli:argv 保留原始字节时必须逐字节命中
// (旧实现经 to_string_lossy 会变成 U+FFFD,从而误拒合法的 npm install)。
{
use std::os::unix::ffi::OsStringExt;
let raw_npm_cli = PathBuf::from(OsString::from_vec(
b"/opt/node/lib/node_modules/npm/bin/n\xffpm-cli.js".to_vec(),
));
let raw_arguments = vec![
raw_npm_cli.as_os_str().to_owned(),
OsString::from("install"),
];
assert!(command_sandbox_requests_npm_install(
node,
&raw_arguments,
Some((node, raw_npm_cli.as_path())),
));
}
}
struct TempTree(PathBuf);
@@ -1492,7 +1506,7 @@ print("SANDBOX_OK")
let launch = prepare_command_sandbox_launch(
&root,
Path::new("/usr/bin/python3"),
&["-c".to_string(), script],
&[OsString::from("-c"), OsString::from(script)],
&root,
&environment,
None,
@@ -1547,7 +1561,7 @@ print("SANDBOX_OK")
(OsString::from("PATH"), runtime.safe_path.clone()),
(OsString::from("HOME"), OsString::from("/host/home")),
];
let run = |executable: &Path, arguments: &[String]| {
let run = |executable: &Path, arguments: &[OsString]| {
let launch = prepare_command_sandbox_launch(
&root,
executable,
@@ -1575,8 +1589,8 @@ print("SANDBOX_OK")
let version = run(
&runtime.node,
&[
"-e".to_string(),
"process.stdout.write(process.version)".to_string(),
OsString::from("-e"),
OsString::from("process.stdout.write(process.version)"),
],
);
assert!(
@@ -1584,8 +1598,13 @@ print("SANDBOX_OK")
"unexpected node version: {version}"
);
let npm_cli = runtime.npm_cli.to_string_lossy().into_owned();
let npm_version = run(&runtime.node, &[npm_cli, "--version".to_string()]);
let npm_version = run(
&runtime.node,
&[
runtime.npm_cli.as_os_str().to_owned(),
OsString::from("--version"),
],
);
assert!(!npm_version.is_empty(), "npm --version returned nothing");
}
@@ -1623,7 +1642,7 @@ print("SANDBOX_OK")
(OsString::from("PATH"), prefix.join("bin").into_os_string()),
(OsString::from("HOME"), OsString::from("/host/home")),
];
let run = |arguments: &[String]| {
let run = |arguments: &[OsString]| {
let launch = prepare_command_sandbox_launch(
&root,
&node,
@@ -1649,17 +1668,14 @@ print("SANDBOX_OK")
};
let version = run(&[
"-e".to_string(),
"process.stdout.write(process.version)".to_string(),
OsString::from("-e"),
OsString::from("process.stdout.write(process.version)"),
]);
assert!(
version.starts_with('v'),
"unexpected node version: {version}"
);
let npm_version = run(&[
npm_cli.to_string_lossy().into_owned(),
"--version".to_string(),
]);
let npm_version = run(&[npm_cli.as_os_str().to_owned(), OsString::from("--version")]);
assert!(!npm_version.is_empty(), "npm --version returned nothing");
}
@@ -1679,7 +1695,11 @@ print("SANDBOX_OK")
"from pathlib import Path; import os; assert os.readlink('/proc/self/fd/0') == '/dev/null'; assert all(not Path(f'/proc/self/fd/{{fd}}').exists() for fd in (3, 4, 5, 6)); Path({:?}).write_text('COMMITTED')",
marker.to_string_lossy()
);
let arguments = vec!["-c".to_string(), script, "--".to_string()];
let arguments = vec![
OsString::from("-c"),
OsString::from(script),
OsString::from("--"),
];
let environment = vec![(OsString::from("PATH"), OsString::from("/usr/bin"))];
let launch = prepare_linux_command_sandbox_launch(
&root,
@@ -1690,7 +1710,7 @@ print("SANDBOX_OK")
None,
)
.expect("prepare real Linux sandbox");
let target_arguments = arguments.iter().map(OsString::from).collect::<Vec<_>>();
let target_arguments = arguments.clone();
let gate =
LaunchGate::new_for_sandbox_stdin(Path::new("/usr/bin/python3"), &target_arguments)
.expect("create real Linux sandbox gate");
@@ -73,7 +73,7 @@ mod linux {
target_executable: target_executable.as_os_str().as_bytes().to_vec(),
target_arguments: target_arguments
.into_iter()
.map(|argument| OsString::from(argument).into_vec())
.map(|argument| argument.into_vec())
.collect(),
})
}