diff --git a/apps/ai-game-creator-shell/src-tauri/src/command_exec.rs b/apps/ai-game-creator-shell/src-tauri/src/command_exec.rs index d5a9582b5..01f2d1638 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/command_exec.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/command_exec.rs @@ -961,19 +961,24 @@ fn resolve_project_command_executable( } /// Linux 上 npm 以 `node ` 启动;其余情况保持 `executable + args`。 -pub(crate) fn project_command_actual_target(spec: &ProjectCommandSpec) -> (PathBuf, Vec) { +pub(crate) fn project_command_actual_target(spec: &ProjectCommandSpec) -> (PathBuf, Vec) { #[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")] diff --git a/apps/ai-game-creator-shell/src-tauri/src/command_sandbox.rs b/apps/ai-game-creator-shell/src-tauri/src/command_sandbox.rs index 8a1d0e63e..7678a5dea 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/command_sandbox.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/command_sandbox.rs @@ -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, + arguments: Vec, 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::>(); + 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"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/process_session_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/process_session_bridge.rs index 040ed3d36..c3be2b6e6 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/process_session_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/process_session_bridge.rs @@ -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(), }) }