diff options
| -rw-r--r-- | crates/shirabe-php-shim/src/phar.rs | 36 | ||||
| -rw-r--r-- | docs/dev/composer-runtime-bundle.md | 3 | ||||
| -rw-r--r-- | scripts/linters/src/Linters/NoHaltCompilerLiteral.php | 4 |
3 files changed, 32 insertions, 11 deletions
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<Vec<PharEntry>> { 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<u8> { + /// signature) following the php.net manual layout, with `token` as the stub token. + fn build_native_phar(tampered: bool, token: [u8; 18]) -> Vec<u8> { 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"<?php ".to_vec(); - bytes.extend_from_slice(&halt_compiler_token()); + bytes.extend_from_slice(&token); bytes.extend_from_slice(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"); @@ -1040,10 +1040,30 @@ 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!( diff --git a/docs/dev/composer-runtime-bundle.md b/docs/dev/composer-runtime-bundle.md index f152897f..19d49bce 100644 --- a/docs/dev/composer-runtime-bundle.md +++ b/docs/dev/composer-runtime-bundle.md @@ -27,7 +27,8 @@ cached per file, and the bundle is never re-verified. A phar file consists of 3 or 4 sections: a stub, a manifest, the actual contents and an optional signature. The stub and the manifest are separated by `__HALT_COMPILER();` tokens, which means that Shirabe's executable binary -must not contain the tokens except for phar's one. +must not contain the tokens except for phar's one. Note that the token is +case-insensitive. ## Accessing files at runtime diff --git a/scripts/linters/src/Linters/NoHaltCompilerLiteral.php b/scripts/linters/src/Linters/NoHaltCompilerLiteral.php index 30520ff7..30160886 100644 --- a/scripts/linters/src/Linters/NoHaltCompilerLiteral.php +++ b/scripts/linters/src/Linters/NoHaltCompilerLiteral.php @@ -20,7 +20,7 @@ final class NoHaltCompilerLiteral implements Linter public function failureIntro(): string { - return "Found a literal `" . self::TOKEN . "`.\n" + return "Found a literal `" . self::TOKEN . "` (case-insensitive).\n" . "The executable carries the Composer runtime bundle as a phar that PHP finds by\n" . "scanning for the first occurrence of that token, and every literal here ends up in\n" . "the same binary, so an earlier one shadows the bundle. Build the token at run time\n" @@ -39,7 +39,7 @@ final class NoHaltCompilerLiteral implements Linter } foreach (file($path) as $idx => $raw) { - if (!str_contains($raw, self::TOKEN)) { + if (stripos($raw, self::TOKEN) === false) { continue; } |
