mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 17:59:08 +00:00
fix(audit): confirm FAT identity for probed package members
The package probe treated a decoded FAT table as a universal Mach-O. A JVM class past the 4 KiB probe shares the FAT magic, so its truncated prefix decoded as a table, the member counted as an unread native image, and Electron could not be ruled out of a complete scan. Pass the member's declared size to the sniff and take identity from classifyFatContainerEvidence. A member with no usable size, or with slices past the probe, stays counted as possibly native.
This commit is contained in:
2 files changed
+67
-4
No files matched your search
@@ -21,6 +21,7 @@ import { asarHeaderBytes } from "../../tests/fixtures/asarHeader.js";
|
||||
import { binaryPlistObjectTable } from "../../tests/fixtures/binaryPlistObjectTable.js";
|
||||
import { javaClassFile } from "../../tests/fixtures/javaClassFile.js";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { universalMacho } from "../../tests/fixtures/universalMacho.js";
|
||||
import { windowsNativeArtifactFixture } from "../../tests/fixtures/windowsNativeArtifact.js";
|
||||
import {
|
||||
ArtifactReaderFailure,
|
||||
@@ -2021,6 +2022,36 @@ describe("ReleaseExposureReader unexpanded package PE reads", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ReleaseExposureReader probed FAT identity", () => {
|
||||
const omittedNative = async (bytes: Buffer): Promise<number | undefined> => {
|
||||
const root = await bundled([{ path: "member.bin", bytes }]);
|
||||
const scanned = await inspect(root);
|
||||
return scanned.omitted.find(
|
||||
(item) => item.subject === PACKAGED_NATIVE_IMAGES,
|
||||
)?.count;
|
||||
};
|
||||
|
||||
it("keeps a JVM class past the probe out of the packaged native images", async () => {
|
||||
const bytes = javaClassFile(MAX_RELEASE_EXPOSURE_MEMBER_PROBE_BYTES);
|
||||
expect(bytes.length).toBeGreaterThan(
|
||||
MAX_RELEASE_EXPOSURE_MEMBER_PROBE_BYTES,
|
||||
);
|
||||
await expect(omittedNative(bytes)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("counts a universal image whose slice headers sit past the probe", async () => {
|
||||
const slice = Buffer.alloc(64);
|
||||
slice.writeUInt32BE(0xcffaedfe, 0);
|
||||
const { bytes, slices } = universalMacho([
|
||||
{ bytes: slice, cpuType: 0x0100000c, cpuSubtype: 0 },
|
||||
]);
|
||||
expect(slices[0]?.offset).toBeGreaterThanOrEqual(
|
||||
MAX_RELEASE_EXPOSURE_MEMBER_PROBE_BYTES,
|
||||
);
|
||||
await expect(omittedNative(bytes)).resolves.toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ReleaseExposureReader bounded listing and cleanup", () => {
|
||||
it("reports sibling close failure with the completed scan and retains its owner", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
|
||||
@@ -52,6 +52,7 @@ import type {
|
||||
ArtifactOccurrence,
|
||||
} from "../domain/artifactGraph.js";
|
||||
import {
|
||||
classifyFatContainerEvidence,
|
||||
readFatContainerHeader,
|
||||
readMachoHeaderPrefixFacts,
|
||||
} from "../domain/apple/machoContainer.js";
|
||||
@@ -522,6 +523,7 @@ const scanOccurrences = async (
|
||||
occurrence.artifact_kind,
|
||||
),
|
||||
encrypted: occurrence.encrypted,
|
||||
declaredSize: occurrence.declared_size,
|
||||
location,
|
||||
packageFormat:
|
||||
packaged === undefined ? null : asPackageFormat(formatOf(packaged)),
|
||||
@@ -755,6 +757,8 @@ interface MemberView {
|
||||
readonly format: string;
|
||||
readonly kind: string;
|
||||
readonly encrypted: boolean;
|
||||
/** Full member size its owner declared, when one did. */
|
||||
readonly declaredSize: number | null;
|
||||
/** Reported path. A package member names its host file. */
|
||||
readonly location: string;
|
||||
readonly packageFormat: PackageFormat | null;
|
||||
@@ -842,6 +846,7 @@ const examineSelectedFile = async (
|
||||
format: rootFormat,
|
||||
kind: "file",
|
||||
encrypted: false,
|
||||
declaredSize: bytes.length,
|
||||
location: input.path,
|
||||
packageFormat: null,
|
||||
read: (budget) => Promise.resolve(fitAllowance(bytes, budget)),
|
||||
@@ -1413,7 +1418,12 @@ const examinePackagedBinary = async (
|
||||
const header = probe.kind === "bytes" ? probe.bytes : probe.prefix;
|
||||
const native =
|
||||
asNativeFormat(member.format) ??
|
||||
(header === null ? null : sniffNativeFormat(header));
|
||||
(header === null
|
||||
? null
|
||||
: sniffNativeFormat(
|
||||
header,
|
||||
probe.kind === "bytes" ? header.length : member.declaredSize,
|
||||
));
|
||||
if (native === null && header?.toString("latin1", 0, 2) === "MZ") {
|
||||
const windows = observeMzWindowsHeader(header);
|
||||
if (
|
||||
@@ -1753,6 +1763,7 @@ const scanUnexpandedContainer = async (
|
||||
? "javascript"
|
||||
: ownerMemberKind(entry.formatMetadata, "file"),
|
||||
encrypted: entry.encrypted,
|
||||
declaredSize: entry.declaredSize,
|
||||
location,
|
||||
packageFormat: format,
|
||||
read: (budget) =>
|
||||
@@ -2293,8 +2304,29 @@ const hasCompanionName = (logical: string): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* FAT identity from the container owner. A prefix can only rule a universal
|
||||
* image out against the member's real size: without one, or with slices past
|
||||
* the prefix, the image may be native and stays counted as one.
|
||||
*/
|
||||
const sniffFatFormat = (
|
||||
bytes: Buffer,
|
||||
size: number | null,
|
||||
): "mach-o-universal" | null => {
|
||||
const known = size !== null && size >= bytes.length;
|
||||
const header = readFatContainerHeader(bytes, known ? size : bytes.length);
|
||||
if (header.state !== "parsed") return null;
|
||||
return known &&
|
||||
classifyFatContainerEvidence(header, bytes).state === "not-mach-o"
|
||||
? null
|
||||
: "mach-o-universal";
|
||||
};
|
||||
|
||||
/** Header facts from the executable owners. A JVM class is not a FAT Mach-O. */
|
||||
const sniffNativeFormat = (bytes: Buffer): NativeFormat | null => {
|
||||
const sniffNativeFormat = (
|
||||
bytes: Buffer,
|
||||
size: number | null = bytes.length,
|
||||
): NativeFormat | null => {
|
||||
if (
|
||||
bytes.length >= 4 &&
|
||||
bytes[0] === 0x7f &&
|
||||
@@ -2304,8 +2336,8 @@ const sniffNativeFormat = (bytes: Buffer): NativeFormat | null => {
|
||||
)
|
||||
return "elf";
|
||||
if (readMachoHeaderPrefixFacts(bytes).format !== null) return "mach-o";
|
||||
if (readFatContainerHeader(bytes, bytes.length).state === "parsed")
|
||||
return "mach-o-universal";
|
||||
const universal = sniffFatFormat(bytes, size);
|
||||
if (universal !== null) return universal;
|
||||
const windows = observeMzWindowsHeader(bytes);
|
||||
if (windows.state === "declared" && windows.signature === "PE") return "pe";
|
||||
return null;
|
||||
|
||||
Reference in new issue
Block a user