mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 17:30:32 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eb48add14 |
No files matched your search
@@ -135,25 +135,13 @@ def inspect_elf(path, cache):
|
||||
from pwnlib.context import context
|
||||
with context.local(cache_dir=str(cache), log_level="warning"):
|
||||
from elftools.common.exceptions import ELFError
|
||||
from elftools.common.utils import struct_parse
|
||||
from elftools.elf.elffile import ELFFile
|
||||
from pwnlib.elf import ELF
|
||||
try:
|
||||
headers = ELFFile(io.BytesIO(content))
|
||||
section_count = headers.num_sections()
|
||||
if section_count:
|
||||
if headers.header.e_shentsize < headers.structs.Elf_Shdr.sizeof():
|
||||
raise LayoutFailure("format", "ELF section-header entry is shorter than the required structure.")
|
||||
location(headers.header.e_shoff, section_count * headers.header.e_shentsize, len(content))
|
||||
# Upstream string/symbol/relocation access reads stored stream bytes,
|
||||
# while this profile reports logical names and file coordinates.
|
||||
# Admit those encodings before LayoutELF populates convenience maps.
|
||||
for index in range(section_count):
|
||||
raw_header = struct_parse(headers.structs.Elf_Shdr, headers.stream,
|
||||
stream_pos=headers.header.e_shoff + index * headers.header.e_shentsize)
|
||||
if raw_header.sh_flags & 0x800 and raw_header.sh_type in (
|
||||
"SHT_STRTAB", "SHT_SYMTAB", "SHT_DYNSYM", "SHT_REL", "SHT_RELA", "SHT_RELR", "SHT_DYNAMIC"):
|
||||
raise LayoutFailure("unsupported-target", f"ELF section {index} ({raw_header.sh_type}) is SHF_COMPRESSED; logical metadata and its source coordinates are not decoded by this profile.")
|
||||
segment_count = headers.num_segments()
|
||||
if segment_count:
|
||||
location(headers.header.e_phoff, segment_count * headers.header.e_phentsize, len(content))
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
"""Producer admission regressions; no pwntools imports or target execution."""
|
||||
from contextlib import nullcontext
|
||||
import importlib.metadata
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
layout = runpy.run_path(str(Path(__file__).with_name("layout_impl.py")))
|
||||
|
||||
|
||||
class UpstreamReached(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class LayoutStorageTests(unittest.TestCase):
|
||||
def inspect(self, section_type, flags, section_stride=64):
|
||||
class Headers:
|
||||
header = types.SimpleNamespace(e_shoff=64, e_shentsize=section_stride, e_shstrndx=0)
|
||||
structs = types.SimpleNamespace(Elf_Shdr=types.SimpleNamespace(sizeof=lambda: 64))
|
||||
stream = object()
|
||||
|
||||
def num_sections(self): return 2
|
||||
def num_segments(self): return 0
|
||||
def get_shstrndx(self): return 0
|
||||
def _get_section_header(self, index):
|
||||
return types.SimpleNamespace(sh_type="SHT_NULL" if index == 0 else section_type,
|
||||
sh_flags=0 if index == 0 else flags)
|
||||
|
||||
class Elf:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
raise UpstreamReached("No artifact decoding or emulation is allowed in this test")
|
||||
|
||||
modules = {name: types.ModuleType(name) for name in (
|
||||
"pwnlib", "pwnlib.context", "pwnlib.elf", "elftools", "elftools.common",
|
||||
"elftools.common.exceptions", "elftools.common.utils", "elftools.elf", "elftools.elf.elffile")}
|
||||
modules["pwnlib.context"].context = types.SimpleNamespace(local=lambda **_kwargs: nullcontext())
|
||||
modules["pwnlib.elf"].ELF = Elf
|
||||
modules["elftools.common.exceptions"].ELFError = type("ELFError", (Exception,), {})
|
||||
modules["elftools.elf.elffile"].ELFFile = lambda _stream: Headers()
|
||||
modules["elftools.common.utils"].struct_parse = lambda _struct, _stream, stream_pos: Headers()._get_section_header((stream_pos - 64) // max(section_stride, 1))
|
||||
content = bytearray(64 + 2 * max(section_stride, 64))
|
||||
content[:7] = b"\x7fELF\x02\x01\x01"
|
||||
content[16:18] = (1).to_bytes(2, "little") # ET_REL
|
||||
content[18:20] = (62).to_bytes(2, "little")
|
||||
content[40:48] = (64).to_bytes(8, "little")
|
||||
content[58:60] = section_stride.to_bytes(2, "little")
|
||||
content[60:62] = (2).to_bytes(2, "little")
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
snapshot = Path(directory) / "object.snapshot"
|
||||
snapshot.write_bytes(content)
|
||||
with patch.dict(sys.modules, modules), patch.object(importlib.metadata, "version", side_effect=layout["PACKAGES"].__getitem__):
|
||||
return layout["inspect_elf"](snapshot, Path(directory) / "cache")
|
||||
|
||||
def test_compressed_interpreted_storage_refuses_before_upstream_decoding(self):
|
||||
for section_type in ("SHT_STRTAB", "SHT_SYMTAB", "SHT_DYNSYM", "SHT_REL", "SHT_RELA", "SHT_RELR", "SHT_DYNAMIC"):
|
||||
with self.subTest(section_type=section_type):
|
||||
with self.assertRaises(layout["LayoutFailure"]) as raised:
|
||||
self.inspect(section_type, 0x800)
|
||||
self.assertEqual(raised.exception.reason, "unsupported-target")
|
||||
self.assertIn("SHF_COMPRESSED", str(raised.exception))
|
||||
with self.assertRaises(UpstreamReached):
|
||||
self.inspect(section_type, 0)
|
||||
|
||||
def test_uninterpreted_compressed_payload_does_not_change_admission(self):
|
||||
with self.assertRaises(UpstreamReached):
|
||||
self.inspect("SHT_PROGBITS", 0x800)
|
||||
|
||||
def test_short_section_header_stride_refuses_before_raw_header_scan(self):
|
||||
for stride in (0, 1, 63):
|
||||
with self.subTest(stride=stride):
|
||||
with self.assertRaises(layout["LayoutFailure"]) as raised:
|
||||
self.inspect("SHT_PROGBITS", 0, stride)
|
||||
self.assertEqual(raised.exception.reason, "format")
|
||||
self.assertIn("section-header entry", str(raised.exception))
|
||||
for stride in (64, 96):
|
||||
with self.subTest(stride=stride):
|
||||
with self.assertRaises(UpstreamReached):
|
||||
self.inspect("SHT_PROGBITS", 0, stride)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -44,11 +44,6 @@ before dependency seeks. Structural inventory and the convenience-map engine
|
||||
still parse the snapshot separately; eliminating that remaining duplication
|
||||
requires preserving the upstream GOT/PLT and mitigation facts.
|
||||
|
||||
The layout profile returns `unsupported_target` for `SHF_COMPRESSED` string,
|
||||
symbol, relocation or dynamic metadata sections before upstream convenience-map
|
||||
decoding. Their logical names and entry coordinates cannot be read from the
|
||||
compressed file bytes. Other compressed payload sections remain opaque.
|
||||
|
||||
## Interpreting results
|
||||
|
||||
- Artifact path, SHA-256 and size identify original selected bytes. All parsing
|
||||
@@ -483,13 +478,6 @@ section directory with neither a dynamic symbol table nor a dynamic segment
|
||||
reports a complete empty inventory. Mach-O exports are reported by
|
||||
`inspect_macho`.
|
||||
|
||||
ELF export tables marked `SHF_COMPRESSED` are not decompressed. A compressed
|
||||
dynamic symbol table or its string table reports `unsupported` coverage rather
|
||||
than treating stored bytes as symbols or names. Compressed version tables or
|
||||
their string tables keep readable export rows with `partial` coverage and
|
||||
unresolved versions. When version definition storage is compressed, requirement
|
||||
names also remain unknown because unseen definitions may override them.
|
||||
|
||||
`max_file_bytes` defaults to 256 MiB (maximum 512 MiB; CLI `--max-file-bytes`).
|
||||
Export directories in PE headers are read like those in sections. An export or
|
||||
dynamic symbol table above 262144 entries, more than 262144 rows, or more than
|
||||
|
||||
@@ -173,12 +173,5 @@ on one page share a fixed 16 MiB text allowance; a page that exceeds it fails
|
||||
with a resource error whose recovery is a smaller `limit`, because
|
||||
`max_ranges` and `max_file_bytes` do not raise it.
|
||||
|
||||
Compressed symbol or name tables and symbol-table links that do not name a
|
||||
file-backed `SHT_STRTAB` section leave symbol names unknown and coverage
|
||||
incomplete; decoded FDE ranges remain available.
|
||||
|
||||
A compressed dynamic section cannot supply `DT_PLTGOT`. Without another
|
||||
observed GOT base, affected data-relative unwind pointers remain unresolved.
|
||||
|
||||
Malformed ELF headers fail without partial success. This workflow does not
|
||||
execute the image, evaluate CFA programs, or follow indirect pointers.
|
||||
@@ -150,11 +150,6 @@ A symbol name that cannot be read, because its string table is missing or its
|
||||
offset lies outside the table, is reported as no name and makes the report
|
||||
`incomplete` with a counted limitation. Name offset 0 is the empty name.
|
||||
|
||||
ELF sections marked `SHF_COMPRESSED` are not decoded as initializer slots,
|
||||
symbols, names or relocation records. Affected facts remain unresolved and
|
||||
coverage is incomplete; other readable tables remain available. An explicit
|
||||
`DT_SYMTAB` must identify the decoded symbol table used for dynamic relocations.
|
||||
|
||||
The dynamic relocation tables are read in steps that yield to other work and
|
||||
observe cancellation, so a large `DT_RELR` table does not block the server.
|
||||
|
||||
|
||||
@@ -66,6 +66,28 @@ const report: PythonPyc = {
|
||||
],
|
||||
};
|
||||
|
||||
it.each([
|
||||
{
|
||||
flags: 1,
|
||||
hash_based: true,
|
||||
source_hash_hex: "",
|
||||
source_mtime: null,
|
||||
source_size: null,
|
||||
},
|
||||
{ flags: 1 },
|
||||
])("refuses inconsistent provider header facts %j", async (header) => {
|
||||
const result = await new PythonPycService({
|
||||
close: async () => {},
|
||||
identity,
|
||||
inspect: async () =>
|
||||
ok({ ...report, header: { ...report.header, ...header } }),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisOutputError" },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a relative path and extra arguments before reading", async () => {
|
||||
let calls = 0;
|
||||
const service = new PythonPycService({
|
||||
|
||||
@@ -3,9 +3,49 @@ import {
|
||||
inspectPythonPycInputSchema,
|
||||
pythonPycMarshalProfile,
|
||||
PYTHON_PYC_MARSHAL_PROFILES,
|
||||
pythonPycHeaderSchema,
|
||||
} from "./pythonPyc.js";
|
||||
import { pythonBytecodeIdentity } from "./pythonBytecodeIdentity.js";
|
||||
|
||||
const hashHeader = {
|
||||
magic: 3531,
|
||||
python_version: "3.12",
|
||||
code_layout: "py311",
|
||||
flags: 3,
|
||||
hash_based: true,
|
||||
check_source: true,
|
||||
source_hash_hex: "0123456789abcdef",
|
||||
source_mtime: null,
|
||||
source_size: null,
|
||||
};
|
||||
|
||||
it.each(["", "ab", "g".repeat(16)])(
|
||||
"rejects an invalid source hash %j",
|
||||
(source_hash_hex) => {
|
||||
expect(
|
||||
pythonPycHeaderSchema.safeParse({ ...hashHeader, source_hash_hex })
|
||||
.success,
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ hash_based: false },
|
||||
{ check_source: false },
|
||||
{ source_hash_hex: null },
|
||||
{ source_mtime: 0 },
|
||||
{ source_size: 0 },
|
||||
])("keeps header facts consistent with the recorded flags: %j", (changed) => {
|
||||
expect(
|
||||
pythonPycHeaderSchema.safeParse({ ...hashHeader, ...changed }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves unknown raw header flags and valid hash bytes", () => {
|
||||
const header = { ...hashHeader, flags: 0x80000003 };
|
||||
expect(pythonPycHeaderSchema.parse(header)).toEqual(header);
|
||||
});
|
||||
|
||||
it("keeps pinned CPython identity separate from standalone marshal coverage", () => {
|
||||
expect(pythonPycMarshalProfile(3394)).toEqual({
|
||||
code_layout: "py37",
|
||||
|
||||
@@ -205,17 +205,53 @@ const pythonCodeObjectSchema = z.strictObject({
|
||||
});
|
||||
export type PythonPycCodeObject = z.infer<typeof pythonCodeObjectSchema>;
|
||||
|
||||
export const pythonPycHeaderSchema = z.strictObject({
|
||||
magic: z.number().int().min(0).max(0xffff),
|
||||
python_version: z.string().min(1),
|
||||
code_layout: z.enum(PYTHON_PYC_CODE_LAYOUTS),
|
||||
flags: uint,
|
||||
hash_based: z.boolean(),
|
||||
check_source: z.boolean(),
|
||||
source_hash_hex: z.string().nullable(),
|
||||
source_mtime: uint.nullable(),
|
||||
source_size: uint.nullable(),
|
||||
});
|
||||
export const pythonPycHeaderSchema = z
|
||||
.strictObject({
|
||||
magic: z.number().int().min(0).max(0xffff),
|
||||
python_version: z.string().min(1),
|
||||
code_layout: z.enum(PYTHON_PYC_CODE_LAYOUTS),
|
||||
flags: uint,
|
||||
hash_based: z.boolean(),
|
||||
check_source: z.boolean(),
|
||||
source_hash_hex: z
|
||||
.string()
|
||||
.regex(/^[0-9a-f]{16}$/u)
|
||||
.nullable(),
|
||||
source_mtime: uint.nullable(),
|
||||
source_size: uint.nullable(),
|
||||
})
|
||||
.superRefine((header, context) => {
|
||||
for (const [field, flag] of [
|
||||
["hash_based", PYTHON_PYC_HASH_BASED],
|
||||
["check_source", PYTHON_PYC_CHECK_SOURCE],
|
||||
] as const)
|
||||
if (header[field] !== ((header.flags & flag) !== 0))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: [field],
|
||||
message: `${field} disagrees with its recorded header flag.`,
|
||||
});
|
||||
if (header.hash_based === (header.source_hash_hex === null))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["source_hash_hex"],
|
||||
message:
|
||||
"Hash-based pycs require an 8-byte source hash; others must omit it.",
|
||||
});
|
||||
if (header.hash_based === (header.source_mtime !== null))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["source_mtime"],
|
||||
message: "Timestamp pycs require mtime; hash-based pycs must omit it.",
|
||||
});
|
||||
if (header.hash_based === (header.source_size !== null))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["source_size"],
|
||||
message:
|
||||
"Timestamp pycs require source_size; hash-based pycs must omit it.",
|
||||
});
|
||||
});
|
||||
|
||||
/** Bounded CPython 3.7–3.13 pyc observation; marshal is walked, bytecode is not decompiled. */
|
||||
export const pythonPycSchema = z
|
||||
@@ -253,29 +289,6 @@ export const pythonPycSchema = z
|
||||
message: "Marshal byte count disagrees with the header-stripped file.",
|
||||
});
|
||||
}
|
||||
if (report.header.hash_based === (report.header.source_hash_hex === null)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["header", "source_hash_hex"],
|
||||
message:
|
||||
"Hash-based pycs require an 8-byte source hash; others must omit it.",
|
||||
});
|
||||
}
|
||||
if (report.header.hash_based === (report.header.source_mtime !== null)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["header", "source_mtime"],
|
||||
message: "Timestamp pycs require mtime; hash-based pycs must omit it.",
|
||||
});
|
||||
}
|
||||
if (report.header.hash_based === (report.header.source_size !== null)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["header", "source_size"],
|
||||
message:
|
||||
"Timestamp pycs require source_size; hash-based pycs must omit it.",
|
||||
});
|
||||
}
|
||||
if (report.coverage.code_objects !== report.code_objects.length) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
|
||||
@@ -43,7 +43,7 @@ const sectionBytes = (
|
||||
section: NativeSection,
|
||||
budget?: CudaMetadataBudget,
|
||||
): Promise<Buffer> => {
|
||||
if (!section.file_backed || section.compressed)
|
||||
if (!section.file_backed || (section.flags & 0x800n) !== 0n)
|
||||
rejectGpu("GPU metadata section has no supported uncompressed file range");
|
||||
budget?.read(gpuCount(section.size));
|
||||
return source.read(gpuCount(section.offset), gpuCount(section.size));
|
||||
|
||||
@@ -194,7 +194,10 @@ export class GpuInventory {
|
||||
continue;
|
||||
}
|
||||
if (!isGpuSection(section.name)) continue;
|
||||
if (!section.file_backed || section.compressed) {
|
||||
if (
|
||||
!section.file_backed ||
|
||||
(directory.format === "elf" && (section.flags & 0x800n) !== 0n)
|
||||
) {
|
||||
this.discovery = "partial";
|
||||
const message = `GPU section ${section.index} has no supported uncompressed file range`;
|
||||
this.#retain(message);
|
||||
|
||||
@@ -340,7 +340,11 @@ export const inspectAndroidJniLibrary = async (
|
||||
for (const section of directory.sections) {
|
||||
if ((section.flags & 3n) !== 3n || section.size === 0n) continue;
|
||||
active();
|
||||
if (section.compressed || !section.file_backed || section.type === 8) {
|
||||
if (
|
||||
(section.flags & 0x800n) !== 0n ||
|
||||
!section.file_backed ||
|
||||
section.type === 8
|
||||
) {
|
||||
tablesStatus = "partial";
|
||||
unknowns.add(
|
||||
"Allocated writable sections without ordinary file bytes were not scanned; runtime-built tables remain unknown",
|
||||
|
||||
@@ -462,33 +462,6 @@ const findSection = (bytes: Buffer, name: string) => {
|
||||
throw new Error(`fixture has no ${name}`);
|
||||
};
|
||||
|
||||
it.each(["compressed-symbols", "compressed-names", "non-string-names"])(
|
||||
"retains unwind ranges without interpreting %s as ordinary symbol storage",
|
||||
(storage) => {
|
||||
const bytes = elfUnwindFixture({ header: "match" });
|
||||
expect(parse(bytes).ranges[0]?.symbol?.text).toBe("alpha");
|
||||
const section = findSection(
|
||||
bytes,
|
||||
storage === "compressed-symbols" ? ".symtab" : ".strtab",
|
||||
);
|
||||
if (storage === "non-string-names")
|
||||
bytes.writeUInt32LE(1, section.headerOffset + 4);
|
||||
else bytes.writeBigUInt64LE(0x800n, section.headerOffset + 8);
|
||||
|
||||
const report = parse(bytes);
|
||||
expect(report.ranges.map(({ start }) => start)).toEqual([
|
||||
address(UNWIND_FN_A),
|
||||
address(UNWIND_FN_B),
|
||||
]);
|
||||
expect(report.ranges.map(({ symbol }) => symbol)).toEqual([null, null]);
|
||||
expect(report.coverage).toMatchObject({
|
||||
status: "incomplete",
|
||||
ranges_with_symbols: 0,
|
||||
eh_frame_hdr_agreement: "agree",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps .eh_frame ranges when .debug_frame is SHT_NOBITS", () => {
|
||||
const report = parse(
|
||||
elfUnwindFixture({
|
||||
@@ -618,18 +591,6 @@ it("caps symbol lookup across .symtab and .dynsym together", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the symbol scan allowance for later tables when an earlier table has no string table", () => {
|
||||
const bytes = elfUnwindFixture({ header: "match", symbolCap: true });
|
||||
const section = findSection(bytes, ".symtab");
|
||||
bytes.writeUInt32LE(0, section.headerOffset + 40);
|
||||
const report = parse(bytes);
|
||||
expect(report.ranges[0]?.symbol?.text).toBe("alpha");
|
||||
expect(report.coverage.status).toBe("incomplete");
|
||||
expect(report.limitations).not.toContain(
|
||||
"Symbol tables were capped at 65536 combined entries.",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([".eh_frame", ".eh_frame_hdr"] as const)(
|
||||
"reports coverage incomplete when a duplicate %s is ignored",
|
||||
(duplicate) => {
|
||||
@@ -704,22 +665,6 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the GOT unknown when compressed dynamic storage resembles decoded entries", () => {
|
||||
const bytes = elfUnwindFixture({ header: "match", datarel: "dynamic" });
|
||||
const section = findSection(bytes, ".dynamic");
|
||||
// The payload still resembles ordinary DT_PLTGOT/DT_NULL entries, but the
|
||||
// declaration makes it opaque compressed storage, not a decoded table.
|
||||
bytes.writeBigUInt64LE(0x800n, section.headerOffset + 8);
|
||||
const report = parse(bytes);
|
||||
expect(report.ranges.map((range) => range.start)).not.toContain(
|
||||
address(UNWIND_FN_A),
|
||||
);
|
||||
expect(report.coverage.status).toBe("incomplete");
|
||||
expect(report.limitations).toContain(
|
||||
"A DW_EH_PE_datarel .eh_frame value needs a GOT base that could not be determined; that value is unresolved.",
|
||||
);
|
||||
});
|
||||
|
||||
it("caps .eh_frame_hdr rows at the max_ranges budget and marks the comparison incomplete", () => {
|
||||
const report = parse(elfUnwindFixture({ header: "duplicate-row" }), {
|
||||
max_ranges: 2,
|
||||
|
||||
@@ -34,7 +34,6 @@ const AUG_R = 0x52;
|
||||
const AUG_S = 0x53;
|
||||
const AUG_B = 0x42;
|
||||
const SHT_SYMTAB = 2;
|
||||
const SHT_STRTAB = 3;
|
||||
const SHT_DYNAMIC = 6;
|
||||
const SHT_NOBITS = 8;
|
||||
const SHT_DYNSYM = 11;
|
||||
@@ -50,6 +49,7 @@ const SHN_ABS = 0xfff1;
|
||||
const SHN_LORESERVE = 0xff00;
|
||||
const SHF_ALLOC = 2n;
|
||||
const SHF_EXECINSTR = 4n;
|
||||
const SHF_COMPRESSED = 0x800n;
|
||||
|
||||
const {
|
||||
cfa: CFA_LIMITATION,
|
||||
@@ -639,7 +639,7 @@ export const parseElfUnwindRanges = (
|
||||
? ".eh_frame_hdr is declared but not file-backed."
|
||||
: ".eh_frame_hdr extends past the end of the file and was not decoded.",
|
||||
);
|
||||
} else if (hdrSection.compressed) {
|
||||
} else if ((hdrSection.flags & SHF_COMPRESSED) !== 0n) {
|
||||
headerLocation = placement.location;
|
||||
headerStatus = "incomplete";
|
||||
fail(".eh_frame_hdr is SHF_COMPRESSED and was not decompressed.");
|
||||
@@ -705,7 +705,7 @@ export const parseElfUnwindRanges = (
|
||||
} else if (frameLocation === null) {
|
||||
frameStatus = "incomplete";
|
||||
fail(".eh_frame lies outside the file.");
|
||||
} else if (ehSection.compressed) {
|
||||
} else if ((ehSection.flags & SHF_COMPRESSED) !== 0n) {
|
||||
frameStatus = "incomplete";
|
||||
fail(".eh_frame is SHF_COMPRESSED and was not decompressed.");
|
||||
} else {
|
||||
@@ -1135,12 +1135,7 @@ const resolveGotBase = (
|
||||
? undefined
|
||||
: { offset: dynamicProgram.offset, fileSize: dynamicProgram.size }) ??
|
||||
directory.sections
|
||||
.filter(
|
||||
(section) =>
|
||||
section.type === SHT_DYNAMIC &&
|
||||
section.file_backed &&
|
||||
!section.compressed,
|
||||
)
|
||||
.filter((section) => section.type === SHT_DYNAMIC && section.file_backed)
|
||||
.map((section) => ({
|
||||
offset: section.offset,
|
||||
fileSize: section.size,
|
||||
@@ -1819,10 +1814,6 @@ const scanSymbols = (
|
||||
let remaining = MAX_ELF_UNWIND_SYMBOLS;
|
||||
for (const table of tables) {
|
||||
assertArtifactReadActive(signal);
|
||||
if (table.compressed) {
|
||||
fail("A symbol table is SHF_COMPRESSED and was not decompressed.");
|
||||
continue;
|
||||
}
|
||||
const width = table.entry_size === 0n ? standard : Number(table.entry_size);
|
||||
if (width !== standard) {
|
||||
fail(
|
||||
@@ -1838,15 +1829,6 @@ const scanSymbols = (
|
||||
const size = Number(table.size);
|
||||
if (size % width !== 0)
|
||||
fail("A symbol table size is not a multiple of its entry width.");
|
||||
const strings = byIndex.get(table.link);
|
||||
if (strings?.type !== SHT_STRTAB || !strings.file_backed) {
|
||||
fail("A symbol table does not link to a file-backed SHT_STRTAB section.");
|
||||
continue;
|
||||
}
|
||||
if (strings.compressed) {
|
||||
fail("A symbol string table is SHF_COMPRESSED and was not decompressed.");
|
||||
continue;
|
||||
}
|
||||
let count = Math.floor(size / width);
|
||||
if (count > remaining) {
|
||||
fail(
|
||||
@@ -1855,10 +1837,15 @@ const scanSymbols = (
|
||||
count = remaining;
|
||||
}
|
||||
remaining -= count;
|
||||
const stringBytes = bytes.subarray(
|
||||
Number(strings.offset),
|
||||
Number(strings.offset + strings.size),
|
||||
);
|
||||
const strings = byIndex.get(table.link);
|
||||
const stringBytes =
|
||||
strings !== undefined && strings.file_backed
|
||||
? bytes.subarray(
|
||||
Number(strings.offset),
|
||||
Number(strings.offset + strings.size),
|
||||
)
|
||||
: null;
|
||||
if (stringBytes === null) fail("A symbol string table could not be read.");
|
||||
// Observation per string-table offset; null when the name is unreadable.
|
||||
const names = new Map<number, SymbolName | null>();
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
@@ -1891,7 +1878,7 @@ const scanSymbols = (
|
||||
if (owner !== undefined) address += owner.address;
|
||||
}
|
||||
// Only a symbol at a decoded FDE start can name a range.
|
||||
if (!wanted.has(address)) continue;
|
||||
if (stringBytes === null || !wanted.has(address)) continue;
|
||||
let name = names.get(symbol.name_offset);
|
||||
if (name === undefined) {
|
||||
const observed = observeElfStringTableEntry(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { deflateSync } from "node:zlib";
|
||||
|
||||
import {
|
||||
elfExportImage,
|
||||
@@ -23,172 +22,6 @@ const parse = (bytes: Buffer) =>
|
||||
parseNativeExports(bytes, artifact),
|
||||
);
|
||||
|
||||
/** Encode real gABI compressed storage, retaining logical section links. */
|
||||
const compressExportSection = (
|
||||
bytes: Buffer,
|
||||
sectionIndex: number,
|
||||
versionStrings = false,
|
||||
): Buffer => {
|
||||
const wide = bytes[4] === 2;
|
||||
const little = bytes[5] === 1;
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const word = (at: number) =>
|
||||
Number(wide ? view.getBigUint64(at, little) : view.getUint32(at, little));
|
||||
const table = word(wide ? 40 : 32);
|
||||
const width = wide ? 64 : 40;
|
||||
const section = table + sectionIndex * width;
|
||||
const offset = word(section + (wide ? 24 : 16));
|
||||
const size = word(section + (wide ? 32 : 20));
|
||||
const header = Buffer.alloc(wide ? 24 : 12);
|
||||
const chdr = new DataView(
|
||||
header.buffer,
|
||||
header.byteOffset,
|
||||
header.byteLength,
|
||||
);
|
||||
chdr.setUint32(0, 1, little); // ELFCOMPRESS_ZLIB
|
||||
if (wide) {
|
||||
chdr.setBigUint64(8, BigInt(size), little);
|
||||
chdr.setBigUint64(16, 1n, little);
|
||||
} else {
|
||||
chdr.setUint32(4, size, little);
|
||||
chdr.setUint32(8, 1, little);
|
||||
}
|
||||
const storage = Buffer.concat([
|
||||
header,
|
||||
deflateSync(bytes.subarray(offset, offset + size)),
|
||||
]);
|
||||
const count = view.getUint16(wide ? 60 : 48, little);
|
||||
const result = Buffer.concat([
|
||||
bytes,
|
||||
Buffer.alloc(versionStrings ? width : 0),
|
||||
storage,
|
||||
]);
|
||||
const updated = new DataView(
|
||||
result.buffer,
|
||||
result.byteOffset,
|
||||
result.byteLength,
|
||||
);
|
||||
const setWord = (at: number, value: number) =>
|
||||
wide
|
||||
? updated.setBigUint64(at, BigInt(value), little)
|
||||
: updated.setUint32(at, value, little);
|
||||
const target = versionStrings ? table + count * width : section;
|
||||
if (versionStrings) {
|
||||
bytes.copy(result, target, section, section + width);
|
||||
updated.setUint16(wide ? 60 : 48, count + 1, little);
|
||||
// Definitions and requirements use the copied string table; dynsym keeps
|
||||
// its original uncompressed table and still supplies trustworthy names.
|
||||
for (const index of [4, 5])
|
||||
updated.setUint32(
|
||||
table + index * width + (wide ? 40 : 24),
|
||||
count,
|
||||
little,
|
||||
);
|
||||
}
|
||||
setWord(target + 8, 0x800); // SHF_COMPRESSED, without SHF_ALLOC
|
||||
setWord(
|
||||
target + (wide ? 24 : 16),
|
||||
bytes.length + (versionStrings ? width : 0),
|
||||
);
|
||||
setWord(target + (wide ? 32 : 20), storage.length);
|
||||
return result;
|
||||
};
|
||||
|
||||
it.each(
|
||||
([32, 64] as const).flatMap((bits) =>
|
||||
(
|
||||
[
|
||||
"symbols",
|
||||
"strings",
|
||||
"indices",
|
||||
"definitions",
|
||||
"requirements",
|
||||
"version-strings",
|
||||
] as const
|
||||
).map((kind) => ({ bits, kind })),
|
||||
),
|
||||
)(
|
||||
"keeps compressed ELF$bits $kind distinct from logical table bytes",
|
||||
({ bits, kind }) => {
|
||||
for (const little of [false, true]) {
|
||||
const bytes = elfExportImage({
|
||||
bits,
|
||||
little,
|
||||
symbols: [
|
||||
{ name: "defined", version: 2 },
|
||||
{ name: "required", version: 3 },
|
||||
],
|
||||
versionDefinitions: ["lib.so", "DEFINED"],
|
||||
versionRequirements: [{ index: 3, name: "REQUIRED" }],
|
||||
});
|
||||
expect(parse(bytes)).toMatchObject({
|
||||
coverage: { status: "complete", exports: 2 },
|
||||
});
|
||||
const index = {
|
||||
symbols: 1,
|
||||
strings: 2,
|
||||
indices: 3,
|
||||
definitions: 4,
|
||||
requirements: 5,
|
||||
"version-strings": 2,
|
||||
}[kind];
|
||||
const result = parse(
|
||||
compressExportSection(bytes, index, kind === "version-strings"),
|
||||
);
|
||||
if (kind === "symbols" || kind === "strings") {
|
||||
expect(result).toMatchObject({
|
||||
exports: [],
|
||||
coverage: {
|
||||
status: "unsupported",
|
||||
examined_entries: 0,
|
||||
reason: expect.stringContaining("SHF_COMPRESSED"),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
expect(result).toMatchObject({
|
||||
coverage: { status: "partial", exports: 2 },
|
||||
exports: [
|
||||
{
|
||||
name: "defined",
|
||||
version:
|
||||
kind === "indices"
|
||||
? "unknown"
|
||||
: {
|
||||
index: 2,
|
||||
name:
|
||||
kind === "definitions" || kind === "version-strings"
|
||||
? null
|
||||
: "DEFINED",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "required",
|
||||
version:
|
||||
kind === "indices"
|
||||
? "unknown"
|
||||
: {
|
||||
index: 3,
|
||||
name:
|
||||
kind === "definitions" ||
|
||||
kind === "requirements" ||
|
||||
kind === "version-strings"
|
||||
? null
|
||||
: "REQUIRED",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
code: "version_unresolved",
|
||||
message: expect.stringContaining("SHF_COMPRESSED"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["requirements", "definitions"] as const)(
|
||||
"bounds aliased GNU version %s work and keeps exports with unknown versions",
|
||||
(kind) => {
|
||||
@@ -873,12 +706,13 @@ describe("ELF version requirements", () => {
|
||||
});
|
||||
|
||||
it("uses a definition name when a requirement shares its index", () => {
|
||||
const bytes = elfExportImage({
|
||||
symbols: [{ name: "ambiguous", version: 2 }],
|
||||
versionDefinitions: ["libfixture.so", "DEFINITION"],
|
||||
versionRequirements: [{ index: 2, name: "REQUIREMENT" }],
|
||||
});
|
||||
const result = parse(bytes);
|
||||
const result = parse(
|
||||
elfExportImage({
|
||||
symbols: [{ name: "ambiguous", version: 2 }],
|
||||
versionDefinitions: ["libfixture.so", "DEFINITION"],
|
||||
versionRequirements: [{ index: 2, name: "REQUIREMENT" }],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.exports).toMatchObject([
|
||||
{
|
||||
@@ -888,10 +722,6 @@ describe("ELF version requirements", () => {
|
||||
]);
|
||||
expect(result.coverage.status).toBe("complete");
|
||||
expect(result.issues).toEqual([]);
|
||||
expect(parse(compressExportSection(bytes, 4))).toMatchObject({
|
||||
exports: [{ name: "ambiguous", version: { index: 2, name: null } }],
|
||||
coverage: { status: "partial" },
|
||||
});
|
||||
});
|
||||
|
||||
it("retains exports and reports a truncated version-requirement chain", () => {
|
||||
|
||||
@@ -294,22 +294,12 @@ const versionSectionData = (
|
||||
table: { readonly offset: number; readonly size: number };
|
||||
names: Buffer;
|
||||
} => {
|
||||
if (section.compressed)
|
||||
throw new BinaryFormatFailure(
|
||||
"unsupported",
|
||||
`${label} are SHF_COMPRESSED and were not decompressed`,
|
||||
);
|
||||
const strings = sections[section.link];
|
||||
if (strings?.type !== SHT_STRTAB)
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
`${label} do not link to a string table`,
|
||||
);
|
||||
if (strings.compressed)
|
||||
throw new BinaryFormatFailure(
|
||||
"unsupported",
|
||||
`${label} strings are SHF_COMPRESSED and were not decompressed`,
|
||||
);
|
||||
const stringRange = reader.fileRange(
|
||||
strings.offset,
|
||||
strings.size,
|
||||
@@ -555,13 +545,7 @@ function* readVersionNames(
|
||||
}
|
||||
}
|
||||
} catch (cause) {
|
||||
// An unread compressed definition table may replace any requirement
|
||||
// index. Its absence cannot justify falling back to requirement names.
|
||||
if (
|
||||
cause instanceof ElfVersionWorkFailure ||
|
||||
(cause instanceof BinaryFormatFailure && cause.kind === "unsupported")
|
||||
)
|
||||
namesByIndex.clear();
|
||||
if (cause instanceof ElfVersionWorkFailure) namesByIndex.clear();
|
||||
issues.push({
|
||||
code: "version_unresolved",
|
||||
location: hex(verdef.offset),
|
||||
@@ -621,32 +605,6 @@ function* parseElfExports(
|
||||
};
|
||||
const width = directory.bits === 64 ? 24 : 16;
|
||||
const strings = sections[dynsym.link];
|
||||
const compressed = dynsym.compressed
|
||||
? dynsym
|
||||
: strings?.type === SHT_STRTAB && strings.compressed
|
||||
? strings
|
||||
: null;
|
||||
if (compressed !== null) {
|
||||
const reason = `The ELF ${compressed === dynsym ? "dynamic symbol table" : "dynamic string table"} is SHF_COMPRESSED and was not decompressed; export identities remain unknown`;
|
||||
return {
|
||||
...result,
|
||||
exports: [],
|
||||
coverage: {
|
||||
status: "unsupported",
|
||||
reason,
|
||||
examined_entries: 0,
|
||||
exports: 0,
|
||||
},
|
||||
issues: [
|
||||
{
|
||||
code: "table_unavailable",
|
||||
location: hex(compressed.offset),
|
||||
message: reason,
|
||||
},
|
||||
],
|
||||
limitations: ELF_LIMITATIONS,
|
||||
};
|
||||
}
|
||||
if (
|
||||
dynsym.entry_size !== BigInt(width) ||
|
||||
dynsym.size % BigInt(width) !== 0n ||
|
||||
@@ -689,11 +647,6 @@ function* parseElfExports(
|
||||
let versions: number | null = null;
|
||||
if (versym !== undefined) {
|
||||
try {
|
||||
if (versym.compressed)
|
||||
throw new BinaryFormatFailure(
|
||||
"unsupported",
|
||||
"ELF symbol versions are SHF_COMPRESSED and were not decompressed",
|
||||
);
|
||||
if (versym.size !== BigInt(count * 2))
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
|
||||
@@ -1405,40 +1405,13 @@ it("sizes an empty DT_GNU_HASH table only from an agreeing SHT_DYNSYM", () => {
|
||||
strings.readBigUInt64LE(elfSectionAt(strings, SHT_STRTAB) + 32) - 1n,
|
||||
elfSectionAt(strings, SHT_STRTAB) + 32,
|
||||
);
|
||||
const compressedSymbols = Buffer.from(agreeing);
|
||||
compressedSymbols.writeBigUInt64LE(0x800n, section + 8);
|
||||
const compressedStrings = Buffer.from(agreeing);
|
||||
compressedStrings.writeBigUInt64LE(
|
||||
0x800n,
|
||||
elfSectionAt(compressedStrings, SHT_STRTAB) + 8,
|
||||
);
|
||||
const wrongStringType = Buffer.from(agreeing);
|
||||
wrongStringType.writeUInt32LE(
|
||||
1,
|
||||
elfSectionAt(wrongStringType, SHT_STRTAB) + 4,
|
||||
);
|
||||
for (const bytes of [
|
||||
moved,
|
||||
offset,
|
||||
strings,
|
||||
compressedSymbols,
|
||||
compressedStrings,
|
||||
wrongStringType,
|
||||
])
|
||||
for (const bytes of [moved, offset, strings])
|
||||
expect(
|
||||
findRow(readElfHardening(bytes).rows, "stack_protector"),
|
||||
).toMatchObject({
|
||||
state: "unknown",
|
||||
note: expect.stringContaining("DT_GNU_HASH"),
|
||||
});
|
||||
// A loader-owned hash count needs no section fallback, so compressed section
|
||||
// declarations cannot hide imports named by DT_SYMTAB and DT_STRTAB.
|
||||
const hashed = elfHardeningFixture({ stackChk: true });
|
||||
for (const type of [SHT_DYNSYM, SHT_STRTAB])
|
||||
hashed.writeBigUInt64LE(0x800n, elfSectionAt(hashed, type) + 8);
|
||||
expect(findRow(readElfHardening(hashed).rows, "stack_protector").state).toBe(
|
||||
"enabled",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps an unresolved symbol count distinct from an absent table", () => {
|
||||
|
||||
@@ -51,7 +51,6 @@ import {
|
||||
const ET_EXEC = 2;
|
||||
const ET_DYN = 3;
|
||||
const SHT_SYMTAB = 2;
|
||||
const SHT_STRTAB = 3;
|
||||
const SHT_NOTE = 7;
|
||||
const SHT_DYNSYM = 11;
|
||||
const DT_NULL = 0n;
|
||||
@@ -72,6 +71,7 @@ const DF_1_PIE = 0x08000000n;
|
||||
const PF_X = 1;
|
||||
const GNU_PROPERTY_AARCH64_FEATURE_1_AND = 0xc0000000;
|
||||
const GNU_PROPERTY_X86_FEATURE_1_AND = 0xc0000002;
|
||||
const SHF_COMPRESSED = 0x800n;
|
||||
/** Tags hardening reads as a single value. Repeating tags such as DT_NEEDED stay. */
|
||||
const SINGULAR_DYNAMIC_TAGS = new Set<bigint>([
|
||||
DT_HASH,
|
||||
@@ -175,6 +175,7 @@ export const readElfHardening = (
|
||||
assertElfDeclaredRangesFromBuffer(bytes);
|
||||
const { bits, little, machine, programs, sections } = directory;
|
||||
const headerWidth = bits === 64 ? 56 : 32;
|
||||
const symbolWidth = bits === 64 ? 24 : 16;
|
||||
const dynamicWidth = bits === 64 ? 16 : 8;
|
||||
const propertyAlign = bits === 64 ? 8 : 4;
|
||||
const loads = programs.filter(isSpan).filter((program) => program.type === 1);
|
||||
@@ -545,14 +546,10 @@ const agreeingSectionCount = (
|
||||
if (dynsym === undefined) return null;
|
||||
const strings = sections[dynsym.link];
|
||||
if (
|
||||
dynsym.compressed ||
|
||||
strings === undefined ||
|
||||
strings.compressed ||
|
||||
strings.type !== SHT_STRTAB ||
|
||||
dynsym.address !== symtab.value ||
|
||||
strings.address !== dynamic.entries.get(DT_STRTAB)?.value ||
|
||||
strings.size !== dynamic.entries.get(DT_STRSZ)?.value ||
|
||||
strings.offset !== BigInt(dynamic.stringFileOffset ?? -1)
|
||||
strings?.address !== dynamic.entries.get(DT_STRTAB)?.value ||
|
||||
strings?.size !== dynamic.entries.get(DT_STRSZ)?.value ||
|
||||
strings?.offset !== BigInt(dynamic.stringFileOffset ?? -1)
|
||||
)
|
||||
return null;
|
||||
const entry = dynsym.entry_size === 0n ? BigInt(width) : dynsym.entry_size;
|
||||
@@ -900,10 +897,11 @@ const readFeatureNotes = (
|
||||
if (section.type !== SHT_NOTE) continue;
|
||||
// An empty note section owns no bytes, so it has nothing to walk.
|
||||
if (section.size === 0n) continue;
|
||||
if (section.compressed)
|
||||
const compressed = (section.flags & SHF_COMPRESSED) !== 0n;
|
||||
if (compressed)
|
||||
incomplete ??=
|
||||
"A compressed SHT_NOTE section may hold GNU property notes, and its descriptors were not read.";
|
||||
if (section.compressed) {
|
||||
if (compressed) {
|
||||
if (!section.file_backed) continue;
|
||||
otherSpans.push({
|
||||
offset: exact(section.offset, "SHT_NOTE offset"),
|
||||
|
||||
@@ -124,17 +124,11 @@ const stringAt = (
|
||||
return { name: text, missing: false };
|
||||
};
|
||||
|
||||
const stringTableBytes = (
|
||||
const tableBytes = (
|
||||
bytes: Buffer,
|
||||
section: NativeSection | undefined,
|
||||
): Buffer | null => {
|
||||
if (
|
||||
section === undefined ||
|
||||
section.type !== 3 ||
|
||||
!section.file_backed ||
|
||||
section.compressed
|
||||
)
|
||||
return null;
|
||||
if (section === undefined || !section.file_backed) return null;
|
||||
const offset = Number(section.offset);
|
||||
const size = Number(section.size);
|
||||
if (offset < 0 || size < 0 || offset > bytes.length - size) return null;
|
||||
@@ -150,19 +144,9 @@ const readTable = (
|
||||
): {
|
||||
readonly symbols: ElfInitializerSymbol[];
|
||||
readonly truncated: boolean;
|
||||
readonly incomplete: boolean;
|
||||
} => {
|
||||
const width = facts.bits === 64 ? 24 : 16;
|
||||
const strings = stringTableBytes(facts.bytes, sections[section.link]);
|
||||
const compressedStrings = sections[section.link]?.compressed === true;
|
||||
if (compressedStrings)
|
||||
notes.add(
|
||||
"A symbol string table is compressed; its names were not decoded.",
|
||||
);
|
||||
else if (strings === null)
|
||||
notes.add(
|
||||
"A symbol table does not link a readable SHT_STRTAB section; its names were not decoded.",
|
||||
);
|
||||
const strings = tableBytes(facts.bytes, sections[section.link]);
|
||||
const start = Number(section.offset);
|
||||
const declared = section.size / BigInt(width);
|
||||
const count = Number(declared < BigInt(budget) ? declared : BigInt(budget));
|
||||
@@ -193,7 +177,7 @@ const readTable = (
|
||||
type: parsed.value.type,
|
||||
});
|
||||
}
|
||||
return { symbols, truncated, incomplete: nameMissing || strings === null };
|
||||
return { symbols, truncated: truncated || nameMissing };
|
||||
};
|
||||
|
||||
/** Load SHT_SYMTAB and SHT_DYNSYM once. Defined STT_FUNC names win ties. */
|
||||
@@ -239,20 +223,10 @@ export const loadElfSymbolIndex = (
|
||||
sections.forEach((section, index) => {
|
||||
if (truncated) return;
|
||||
if (section.type !== 2 && section.type !== 11) return;
|
||||
if (section.compressed) {
|
||||
notes.add("A symbol table is compressed; its symbols were not decoded.");
|
||||
unreadable = true;
|
||||
return;
|
||||
}
|
||||
const width = BigInt(facts.bits === 64 ? 24 : 16);
|
||||
if (section.entry_size !== 0n && section.entry_size !== width) {
|
||||
notes.add(
|
||||
"A symbol table has an unexpected entry size; its symbols were not decoded.",
|
||||
);
|
||||
unreadable = true;
|
||||
return;
|
||||
}
|
||||
if (!section.file_backed || section.size % width !== 0n) {
|
||||
if (
|
||||
!section.file_backed ||
|
||||
section.size % BigInt(facts.bits === 64 ? 24 : 16) !== 0n
|
||||
) {
|
||||
notes.add(
|
||||
"A symbol table is not stored in the file or its size is not a whole number of entries, so its names were not read.",
|
||||
);
|
||||
@@ -263,7 +237,6 @@ export const loadElfSymbolIndex = (
|
||||
budget -= table.symbols.length;
|
||||
tables.set(index, table.symbols);
|
||||
truncated ||= table.truncated;
|
||||
unreadable ||= table.incomplete;
|
||||
for (const symbol of table.symbols) consider(symbol);
|
||||
});
|
||||
return {
|
||||
@@ -467,11 +440,7 @@ export const sectionRelocations = async (
|
||||
for (const [position, section] of input.sections.entries()) {
|
||||
const encoding = applicable(input, section);
|
||||
if (encoding === null) continue;
|
||||
if (!section.file_backed || section.compressed) {
|
||||
if (section.compressed)
|
||||
input.notes.add(
|
||||
"A relocation section is compressed; its records were not decoded.",
|
||||
);
|
||||
if (!section.file_backed) {
|
||||
skip(section);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ const indexSections = (sections: readonly NativeSection[]): ElfSectionIndex => {
|
||||
: left.index - right.index,
|
||||
);
|
||||
const backed = buildAddressRangeIndex(
|
||||
ranges((section) => section.file_backed && !section.compressed),
|
||||
ranges((section) => section.file_backed),
|
||||
(left, right) => left.index - right.index,
|
||||
);
|
||||
return {
|
||||
@@ -354,8 +354,7 @@ const findSections = (
|
||||
const withinSection =
|
||||
(section: NativeSection, width: number) =>
|
||||
(address: bigint): number | null => {
|
||||
if (!section.file_backed || section.compressed || address < section.address)
|
||||
return null;
|
||||
if (!section.file_backed || address < section.address) return null;
|
||||
const delta = address - section.address;
|
||||
if (delta + BigInt(width) > section.size) return null;
|
||||
return Number(section.offset + delta);
|
||||
@@ -375,13 +374,6 @@ const sectionSource = (
|
||||
section.name.length <= MAX_NATIVE_INITIALIZER_NAME_BYTES
|
||||
? section.name
|
||||
: fallbackName;
|
||||
if (section.compressed) {
|
||||
note(
|
||||
scan,
|
||||
`${label} is compressed. Its initializer slots were not decoded.`,
|
||||
);
|
||||
return { kind, sentinel, detail: label, slots: [] };
|
||||
}
|
||||
if (!section.file_backed) {
|
||||
note(scan, `${label} is not file-backed. Its slots were omitted.`);
|
||||
return { kind, sentinel, detail: label, slots: [] };
|
||||
@@ -679,7 +671,6 @@ const loadWrites = async (
|
||||
};
|
||||
|
||||
const dynamicSymbols = (
|
||||
scan: ElfScan,
|
||||
sections: readonly NativeSection[],
|
||||
tags: ReadonlyMap<bigint, DynTag>,
|
||||
tables: ReadonlyMap<number, readonly ElfInitializerSymbol[]>,
|
||||
@@ -692,11 +683,6 @@ const dynamicSymbols = (
|
||||
(section.type === 11 || section.type === 2),
|
||||
);
|
||||
if (match >= 0) return tables.get(match);
|
||||
note(
|
||||
scan,
|
||||
"DT_SYMTAB does not identify a decoded symbol table; dynamic relocation symbols are unresolved.",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
const dynsym = sections.findIndex((section) => section.type === 11);
|
||||
return dynsym < 0 ? undefined : tables.get(dynsym);
|
||||
@@ -1056,7 +1042,7 @@ export const readElfInitializers = async (
|
||||
loaded,
|
||||
sectionRels.found,
|
||||
sectionRels.clobbered,
|
||||
dynamicSymbols(scan, directory.sections, tags, symbols.tables),
|
||||
dynamicSymbols(directory.sections, tags, symbols.tables),
|
||||
symbols,
|
||||
loaded.trustRaw && sectionRels.complete,
|
||||
directory.sections,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { deflateSync } from "node:zlib";
|
||||
import { readNativeInitializers } from "./NativeInitializersProvider.js";
|
||||
|
||||
/** Pass-through spies on the shared mapping module, so tests can count lookups made from other modules. */
|
||||
@@ -321,106 +320,6 @@ describe("ELF slot semantics", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ELF section content admission", () => {
|
||||
it("keeps symbol relocations unresolved for a conflicting symbol entry width", async () => {
|
||||
const bytes = elfRelocatableCtors();
|
||||
bytes.writeBigUInt64LE(16n, 0x500 + 4 * 64 + 56);
|
||||
const report = await read(bytes);
|
||||
expect(
|
||||
report.entries.find((entry) => entry.kind === "init_array"),
|
||||
).toMatchObject({
|
||||
resolution: "unresolved",
|
||||
value_va: null,
|
||||
symbol: null,
|
||||
});
|
||||
expect(report.status).toBe("incomplete");
|
||||
expect(report.limitations.join("\n")).toContain("entry size");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ section: 7, label: "initializer array" },
|
||||
{ section: 4, label: "symbol table" },
|
||||
{ section: 5, label: "symbol string table" },
|
||||
{ section: 8, label: "relocation table" },
|
||||
])("preserves uncertainty for a compressed $label", async ({ section }) => {
|
||||
const base = elfRelocatableCtors();
|
||||
const at = 0x500 + section * 64;
|
||||
const offset = Number(base.readBigUInt64LE(at + 24));
|
||||
const size = Number(base.readBigUInt64LE(at + 32));
|
||||
const header = Buffer.alloc(24);
|
||||
header.writeUInt32LE(1, 0);
|
||||
header.writeBigUInt64LE(BigInt(size), 8);
|
||||
header.writeBigUInt64LE(1n, 16);
|
||||
const compressed = Buffer.concat([
|
||||
header,
|
||||
deflateSync(base.subarray(offset, offset + size)),
|
||||
]);
|
||||
const bytes = Buffer.concat([base, compressed]);
|
||||
bytes.writeBigUInt64LE(base.readBigUInt64LE(at + 8) | 0x800n, at + 8);
|
||||
bytes.writeBigUInt64LE(BigInt(base.length), at + 24);
|
||||
bytes.writeBigUInt64LE(BigInt(compressed.length), at + 32);
|
||||
const report = await read(bytes);
|
||||
expect(report.status).toBe("incomplete");
|
||||
expect(report.coverage.total_entries).toBeNull();
|
||||
expect(report.limitations.join("\n")).toContain("compressed");
|
||||
if (section === 7)
|
||||
expect(report.entries.some((entry) => entry.kind === "init_array")).toBe(
|
||||
false,
|
||||
);
|
||||
else {
|
||||
const init = report.entries.find((entry) => entry.kind === "init_array");
|
||||
expect(init).toBeDefined();
|
||||
if (section === 5) expect(init?.symbol).toBeNull();
|
||||
else
|
||||
expect(init).toMatchObject({
|
||||
resolution: "unresolved",
|
||||
value_va: null,
|
||||
});
|
||||
}
|
||||
// The uncompressed fixture remains the working comparison.
|
||||
expect((await read(base)).status).toBe("parsed");
|
||||
});
|
||||
|
||||
it("does not substitute an unrelated dynamic symbol table for DT_SYMTAB", async () => {
|
||||
const bytes = elf64PieInitializers();
|
||||
bytes.writeBigUInt64LE(0x777n, 0x300 + 14 * 16 + 8);
|
||||
const report = await read(bytes);
|
||||
expect(
|
||||
report.entries.find((entry) => entry.slot_va === "0x210"),
|
||||
).toMatchObject({
|
||||
resolution: "unresolved",
|
||||
symbol: null,
|
||||
value_va: null,
|
||||
});
|
||||
expect(report.status).toBe("incomplete");
|
||||
});
|
||||
|
||||
it.each(["compressed", "not a string table"])(
|
||||
"retains later symbol tables when an earlier name source is %s",
|
||||
async (source) => {
|
||||
const bytes = elf64PieInitializers();
|
||||
// The first (dynamic) symbol table links section 8; its bytes still contain imp_init.
|
||||
if (source === "compressed")
|
||||
bytes.writeBigUInt64LE(0x802n, 0x800 + 8 * 64 + 8);
|
||||
else bytes.writeUInt32LE(1, 0x800 + 8 * 64 + 4);
|
||||
const report = await read(bytes);
|
||||
expect(report.status).toBe("incomplete");
|
||||
expect(
|
||||
report.entries.find((entry) => entry.kind === "init"),
|
||||
).toMatchObject({
|
||||
symbol: { name: "ctor_a" },
|
||||
value_va: "0x100",
|
||||
});
|
||||
expect(
|
||||
report.entries.find((entry) => entry.slot_va === "0x210"),
|
||||
).toMatchObject({
|
||||
resolution: "unresolved_import",
|
||||
symbol: null,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("ELF incomplete inputs", () => {
|
||||
it("reports an unsupported relocation machine as incomplete, not as raw zeros", async () => {
|
||||
const bytes = elf64PieInitializers();
|
||||
|
||||
@@ -68,7 +68,8 @@ export const readElfImage = (
|
||||
}
|
||||
for (const section of directory.sections) {
|
||||
const range = declaredFileRange(bytes.length, section.offset, section.size);
|
||||
const { file_backed, compressed } = section;
|
||||
const file_backed = section.type !== 8 && range.file_backed;
|
||||
const compressed = (section.flags & 0x800n) !== 0n;
|
||||
const read_only =
|
||||
section.type === 1 &&
|
||||
file_backed &&
|
||||
|
||||
@@ -2,7 +2,6 @@ import { expect, it } from "vitest";
|
||||
import { pe } from "../../domain/binaryTarget.fixture.js";
|
||||
import { elfHardeningFixture } from "../hardening/BinaryHardening.fixture.js";
|
||||
import { elfUnwindSectionlessFixture } from "../elf/ElfUnwindTables.fixture.js";
|
||||
import { compressedElfNoteSectionFixture } from "../elf/ElfNotes.fixture.js";
|
||||
import {
|
||||
ELF_PT_GNU_EH_FRAME,
|
||||
ELF_PT_GNU_RELRO,
|
||||
@@ -95,35 +94,3 @@ it("retains a confirmed overlapping PE when the COFF record extends past EOF", (
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[32, true],
|
||||
[32, false],
|
||||
[64, true],
|
||||
[64, false],
|
||||
] as const)(
|
||||
"retains ELF%i little=%s compression independently of stored section coverage",
|
||||
(bits, little) => {
|
||||
const bytes = compressedElfNoteSectionFixture(bits, little);
|
||||
const initial = readElfDirectoryFromBuffer(bytes).sections[1]!;
|
||||
expect(initial).toMatchObject({ compressed: true, file_backed: true });
|
||||
expect(readElfDirectoryFromBuffer(bytes).sections[2]).toMatchObject({
|
||||
compressed: false,
|
||||
file_backed: true,
|
||||
});
|
||||
// Compression metadata describes the encoding even if its stored range
|
||||
// subsequently proves incomplete. It must not be inferred from readability.
|
||||
const sizeAt = initial.header_offset + (bits === 64 ? 32 : 20);
|
||||
if (bits === 64) {
|
||||
if (little) bytes.writeBigUInt64LE(BigInt(bytes.length), sizeAt);
|
||||
else bytes.writeBigUInt64BE(BigInt(bytes.length), sizeAt);
|
||||
} else if (little) bytes.writeUInt32LE(bytes.length, sizeAt);
|
||||
else bytes.writeUInt32BE(bytes.length, sizeAt);
|
||||
expect(readElfDirectoryFromBuffer(bytes).sections[1]).toMatchObject({
|
||||
compressed: true,
|
||||
file_backed: false,
|
||||
offset: initial.offset,
|
||||
size: BigInt(bytes.length),
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -45,10 +45,7 @@ export interface NativeSection {
|
||||
readonly entry_size: bigint;
|
||||
/** `sh_addralign` for ELF. PE sections have no note alignment and use 1. */
|
||||
readonly alignment: bigint;
|
||||
/** Declared stored bytes are present; compressed bytes are not decoded content. */
|
||||
readonly file_backed: boolean;
|
||||
/** ELF SHF_COMPRESSED storage encoding; always false for PE sections. */
|
||||
readonly compressed: boolean;
|
||||
}
|
||||
/** Program header types retained for load, dynamic, hardening, and unwind consumers. */
|
||||
export const ELF_PT_NOTE = 4 as const;
|
||||
@@ -311,7 +308,6 @@ export function* readElfSectionDirectory(
|
||||
alignment: section.alignment,
|
||||
file_backed:
|
||||
type !== 8 && declaredFileRange(fileSize, offset, size).file_backed,
|
||||
compressed: (section.flags & 0x800n) !== 0n,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -414,7 +410,6 @@ export function* readPeSectionDirectory(
|
||||
BigInt(row.offset),
|
||||
BigInt(row.size),
|
||||
).file_backed,
|
||||
compressed: false,
|
||||
});
|
||||
}
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user