From 71b0524fcefff426141c28d7ef9807fde36fbb4e Mon Sep 17 00:00:00 2001 From: phatloi0010 Date: Mon, 2 Nov 2026 23:12:43 +0700 Subject: [PATCH] Only treat code/module CArchive members as PyInstaller crypto_key markers (#398) Only code and module members named crypto_key mark PyInstaller modules as encrypted; a data file with that name no longer does. Fixes #334. --- crates/formats/src/pyinstaller.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/crates/formats/src/pyinstaller.rs b/crates/formats/src/pyinstaller.rs index 18dd7a04..77f096d5 100644 --- a/crates/formats/src/pyinstaller.rs +++ b/crates/formats/src/pyinstaller.rs @@ -281,6 +281,7 @@ impl PackedFormat for PyInstaller { find_cookie(data).is_some() } + /// List CArchive members and nested PYZ module entries with archive metadata. fn list(&self, data: &[u8]) -> Result { let cookie = find_cookie(data) .ok_or_else(|| FormatError::Malformed("no sound PyInstaller cookie (its magic, sizes and Python version must all agree)".into()))?; @@ -291,7 +292,7 @@ impl PackedFormat for PyInstaller { notes.push(format!("{}, so the members after it aren't listed.", capitalise(&why))); truncated = true; } - let key_seen = members.iter().any(|m| m.entry.path.contains("crypto_key")); + let key_seen = members.iter().any(|m| matches!(m.type_code, b'm' | b'M' | b's') && m.entry.path.contains("crypto_key")); let scripts: Vec<&str> = members.iter().filter(|m| m.type_code == b's').map(|m| m.entry.path.as_str()).collect(); let entry_scripts: Vec<&str> = scripts.iter().copied().filter(|s| !s.starts_with("pyi_rth_") && !s.starts_with("pyiboot")).collect(); let options: Vec<&str> = members.iter().filter(|m| m.type_code == b'o').map(|m| m.entry.path.as_str()).collect(); @@ -348,9 +349,15 @@ impl PackedFormat for PyInstaller { Ok(PackedListing { facts, entries, notes, truncated }) } + /// Extract a CArchive member or PYZ module within `max_bytes`, including any `.pyc` header. fn read(&self, data: &[u8], entry: &PackedEntry, max_bytes: u64) -> Result, FormatError> { + const PYC_HEADER_BYTES: u64 = 16; let magic = entry.kind.as_deref().and_then(|k| k.strip_prefix(PYZ_KIND)).and_then(|m| u16::from_str_radix(m, 16).ok()); - let body = read_stored(data, entry, max_bytes)?; + let body_limit = match magic { + Some(_) => max_bytes.checked_sub(PYC_HEADER_BYTES).ok_or_else(|| FormatError::TooLarge(format!("{} needs a .pyc header", entry.path)))?, + None => max_bytes, + }; + let body = read_stored(data, entry, body_limit)?; match magic { Some(magic) => Ok(pyc::wrap_marshal(magic, &body)), None => Ok(body), @@ -429,6 +436,7 @@ mod tests { assert_eq!(PyInstaller.read(&data, find(&l, "libz.so.1"), 100).unwrap(), b"native"); } + /// Verify that code/module crypto_key markers flag encryption while data members do not. #[test] fn encrypted_modules_are_flagged_and_listed_but_not_read() { // A key module in the archive. @@ -446,6 +454,15 @@ mod tests { let z = pyz(3531, &[("a", 0, &[0xee; 40])], false, false); let data = pyinstaller(&[("PYZ.pyz", b'z', &z, false)], true, 312); assert_eq!(PyInstaller.list(&data).unwrap().facts["pyz_archives"][0]["encrypted"], true); + + // An ordinary data member named crypto_key.txt does not mark unencrypted PYZ modules unreadable. + let z = pyz(3531, &[("a", 0, &code)], false, true); + let data = pyinstaller(&[("crypto_key.txt", b'x', b"plain", false), ("PYZ.pyz", b'z', &z, false)], true, 312); + let l = PyInstaller.list(&data).unwrap(); + assert_eq!(l.facts["encrypted_modules"], false); + let a = find(&l, "PYZ.pyz.modules/a.pyc"); + assert_eq!(a.compression, Compression::Zlib); + assert!(pyc::read_pyc(&PyInstaller.read(&data, a, 1000).unwrap()).unwrap().code.is_some()); } #[test] @@ -562,11 +579,17 @@ mod tests { assert_eq!((kept.len(), total), (2, 3)); } + /// Verify that `read` enforces `max_bytes` including the 16-byte `.pyc` header for PYZ modules. #[test] fn reads_stop_at_their_limit() { let data = pyinstaller_sample(); let l = PyInstaller.list(&data).unwrap(); assert!(matches!(PyInstaller.read(&data, find(&l, "struct"), 10), Err(FormatError::TooLarge(_)))); + let util = find(&l, "PYZ-00.pyz.modules/pkg/util.pyc"); + let full = PyInstaller.read(&data, util, 10_000).unwrap(); + assert!(matches!(PyInstaller.read(&data, util, 15), Err(FormatError::TooLarge(_)))); + assert!(matches!(PyInstaller.read(&data, util, (full.len() - 1) as u64), Err(FormatError::TooLarge(_)))); + assert_eq!(PyInstaller.read(&data, util, full.len() as u64).unwrap(), full); } #[test]