From 55f385450407a3d8c6c7c90ec4dc8995f5277a94 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 15 Aug 2026 07:50:54 +0900 Subject: fix(phar): match the stub token in any letter case `__halt_compiler` is a PHP keyword, so a stub may spell the token in any letter case. The native phar reader compared the bytes exactly and rejected such an archive, and the lint that keeps a second token out of the executable missed lowercase ones, which would shadow the embedded bundle. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe-php-shim/src/phar.rs | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) (limited to 'crates') diff --git a/crates/shirabe-php-shim/src/phar.rs b/crates/shirabe-php-shim/src/phar.rs index ecd842f3..1815cbf1 100644 --- a/crates/shirabe-php-shim/src/phar.rs +++ b/crates/shirabe-php-shim/src/phar.rs @@ -289,8 +289,8 @@ fn verify_phar_signature(path: &std::path::Path, bytes: &[u8]) -> anyhow::Result /// The special token to separate a phar stub and contents. /// /// The Shirabe executable embeds the Composer runtime bundle as a phar archive, so the token must -/// not appear in the binary. See `docs/dev/composer-runtime-bundle.md`. We use `black_box()` to -/// prevent the Rust compiler from inlining the function to a constant. +/// not appear in the binary in any letter case. See `docs/dev/composer-runtime-bundle.md`. We use +/// `black_box()` to prevent the Rust compiler from inlining the function to a constant. fn halt_compiler_token() -> [u8; 18] { let mut token = *std::hint::black_box(b"__UNYG_PBZCVYRE();"); for byte in &mut token { @@ -308,7 +308,7 @@ fn parse_native_phar(path: &std::path::Path) -> anyhow::Result> { let halt = halt_compiler_token(); let halt_pos = bytes .windows(halt.len()) - .position(|window| window == halt) + .position(|window| window.eq_ignore_ascii_case(&halt)) .ok_or_else(|| { corruption_error( path, @@ -958,8 +958,8 @@ mod tests { } /// Builds a minimal native phar (one stored file, one deflated file, SHA-1 - /// signature) following the php.net manual layout. - fn build_native_phar(tampered: bool) -> Vec { + /// signature) following the php.net manual layout, with `token` as the stub token. + fn build_native_phar(tampered: bool, token: [u8; 18]) -> Vec { let stored = (b"Hello World".to_vec(), "dir/hello.txt"); let big = "abc".repeat(1000).into_bytes(); let mut deflated = Vec::new(); @@ -1003,7 +1003,7 @@ mod tests { } let mut bytes = b"\r\n"); bytes.extend_from_slice(&(manifest.len() as u32).to_le_bytes()); bytes.extend_from_slice(&manifest); @@ -1024,7 +1024,7 @@ mod tests { fn phar_native_read_and_extract() { let dir = tempfile::tempdir().unwrap(); let phar_path = dir.path().join("test.phar"); - std::fs::write(&phar_path, build_native_phar(false)).unwrap(); + std::fs::write(&phar_path, build_native_phar(false, halt_compiler_token())).unwrap(); let phar = Phar::new(&phar_path).unwrap(); let out = dir.path().join("extracted"); @@ -1039,11 +1039,31 @@ mod tests { ); } + #[test] + fn phar_native_stub_token_is_read_in_any_letter_case() { + let dir = tempfile::tempdir().unwrap(); + let mut token = halt_compiler_token(); + token.make_ascii_lowercase(); + let phar_path = dir.path().join("lowercase.phar"); + std::fs::write(&phar_path, build_native_phar(false, token)).unwrap(); + + let out = dir.path().join("extracted"); + Phar::new(&phar_path) + .unwrap() + .extract_to(&out, None, true) + .unwrap(); + + assert_eq!( + std::fs::read(out.join("dir/hello.txt")).unwrap(), + b"Hello World" + ); + } + #[test] fn phar_native_broken_signature_is_rejected() { let dir = tempfile::tempdir().unwrap(); let phar_path = dir.path().join("tampered.phar"); - std::fs::write(&phar_path, build_native_phar(true)).unwrap(); + std::fs::write(&phar_path, build_native_phar(true, halt_compiler_token())).unwrap(); let error = Phar::new(&phar_path).unwrap_err(); assert!( -- cgit v1.3.1-4-g156e