diff --git a/backend/crates/gateway-host/src/system_update/archive.rs b/backend/crates/gateway-host/src/system_update/archive.rs index 1ae5f9480..63d22455d 100644 --- a/backend/crates/gateway-host/src/system_update/archive.rs +++ b/backend/crates/gateway-host/src/system_update/archive.rs @@ -86,11 +86,7 @@ pub(crate) fn extract_release( .unpack(&target) .map_err(|error| internal("无法解压官方插件文件").with_source(error))?; #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt as _; - fs::set_permissions(&target, fs::Permissions::from_mode(0o444)) - .map_err(|error| internal("无法设置官方插件文件权限").with_source(error))?; - } + ensure_official_plugin_file_readonly(&target)?; found_official_manifest |= relative == Path::new("plugin-release-manifest.json"); continue; } @@ -128,6 +124,24 @@ pub(crate) fn extract_release( }) } +#[cfg(unix)] +fn ensure_official_plugin_file_readonly(path: &Path) -> Result<(), OperationError> { + use std::os::unix::fs::PermissionsExt as _; + + // tar 解包已对打开的文件描述符套用归档模式 + // Docker Desktop 把 macOS 目录挂进容器后,0444 文件再次 chmod 会返回 EPERM,权限已满足时不必再改 + let current_mode = fs::symlink_metadata(path) + .map_err(|error| internal("无法设置官方插件文件权限").with_source(error))? + .permissions() + .mode() + & 0o777; + if current_mode == 0o444 { + return Ok(()); + } + fs::set_permissions(path, fs::Permissions::from_mode(0o444)) + .map_err(|error| internal("无法设置官方插件文件权限").with_source(error)) +} + fn unsafe_archive_path(path: &Path) -> bool { path.is_absolute() || path.components().any(|component| match component { diff --git a/backend/crates/gateway-host/tests/system_update/mod.rs b/backend/crates/gateway-host/tests/system_update/mod.rs index a5db50022..8b5471eb1 100644 --- a/backend/crates/gateway-host/tests/system_update/mod.rs +++ b/backend/crates/gateway-host/tests/system_update/mod.rs @@ -419,13 +419,14 @@ enum ChecksumKind { fn release_archive(kind: ArchiveKind) -> Vec { let encoder = GzEncoder::new(Vec::new(), Compression::default()); let mut tar = Builder::new(encoder); - append_file(&mut tar, BINARY_NAME, b"new-binary", false); - append_file(&mut tar, "web/dist/index.html", b"new-web", false); + append_file(&mut tar, BINARY_NAME, b"new-binary", 0o755, false); + append_file(&mut tar, "web/dist/index.html", b"new-web", 0o755, false); if !matches!(kind, ArchiveKind::MissingOfficialManifest) { append_file( &mut tar, "plugins/official/plugin-release-manifest.json", b"new-manifest", + 0o444, false, ); } @@ -433,16 +434,18 @@ fn release_archive(kind: ArchiveKind) -> Vec { &mut tar, "plugins/official/new-plugin.tar.gz", b"new-plugin", + 0o644, false, ); if let ArchiveKind::UnsafePath(name) = kind { - append_file(&mut tar, name, b"escape", true); + append_file(&mut tar, name, b"escape", 0o755, true); } if matches!(kind, ArchiveKind::NestedOfficialAsset) { append_file( &mut tar, "plugins/official/nested/plugin.tar.gz", b"nested", + 0o444, false, ); } @@ -468,14 +471,16 @@ fn append_symlink(tar: &mut Builder>>, name: &str, target: &st tar.append(&header, std::io::empty()).expect("append link"); } -fn append_file(tar: &mut Builder>>, name: &str, data: &[u8], unsafe_path: bool) { +fn append_file( + tar: &mut Builder>>, + name: &str, + data: &[u8], + mode: u32, + unsafe_path: bool, +) { let mut header = Header::new_gnu(); header.set_entry_type(EntryType::Regular); - header.set_mode(if name.starts_with("plugins/official/") { - 0o444 - } else { - 0o755 - }); + header.set_mode(mode); header.set_size(u64::try_from(data.len()).expect("size")); header .set_path(if unsafe_path { "safe" } else { name }) diff --git a/backend/crates/gateway-host/tests/system_update/operations.rs b/backend/crates/gateway-host/tests/system_update/operations.rs index 268b48c34..811b37070 100644 --- a/backend/crates/gateway-host/tests/system_update/operations.rs +++ b/backend/crates/gateway-host/tests/system_update/operations.rs @@ -471,6 +471,17 @@ async fn rollback_should_restore_binary_web_and_version_state() { & 0o777, 0o555 ); + for name in ["plugin-release-manifest.json", "new-plugin.tar.gz"] { + assert_eq!( + fs::metadata(fixture.official().join(name)) + .expect("official plugin file") + .permissions() + .mode() + & 0o777, + 0o444, + "{name}" + ); + } } service .rollback(Arc::new(AllowingUpdatePreflight))