让 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 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:
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user