mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 17:30:32 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e0a43ee6e | ||
|
|
0c10e470e6 |
No files matched your search
@@ -135,13 +135,25 @@ def inspect_elf(path, cache):
|
|||||||
from pwnlib.context import context
|
from pwnlib.context import context
|
||||||
with context.local(cache_dir=str(cache), log_level="warning"):
|
with context.local(cache_dir=str(cache), log_level="warning"):
|
||||||
from elftools.common.exceptions import ELFError
|
from elftools.common.exceptions import ELFError
|
||||||
|
from elftools.common.utils import struct_parse
|
||||||
from elftools.elf.elffile import ELFFile
|
from elftools.elf.elffile import ELFFile
|
||||||
from pwnlib.elf import ELF
|
from pwnlib.elf import ELF
|
||||||
try:
|
try:
|
||||||
headers = ELFFile(io.BytesIO(content))
|
headers = ELFFile(io.BytesIO(content))
|
||||||
section_count = headers.num_sections()
|
section_count = headers.num_sections()
|
||||||
if section_count:
|
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))
|
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()
|
segment_count = headers.num_segments()
|
||||||
if segment_count:
|
if segment_count:
|
||||||
location(headers.header.e_phoff, segment_count * headers.header.e_phentsize, len(content))
|
location(headers.header.e_phoff, segment_count * headers.header.e_phentsize, len(content))
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""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,6 +44,11 @@ before dependency seeks. Structural inventory and the convenience-map engine
|
|||||||
still parse the snapshot separately; eliminating that remaining duplication
|
still parse the snapshot separately; eliminating that remaining duplication
|
||||||
requires preserving the upstream GOT/PLT and mitigation facts.
|
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
|
## Interpreting results
|
||||||
|
|
||||||
- Artifact path, SHA-256 and size identify original selected bytes. All parsing
|
- Artifact path, SHA-256 and size identify original selected bytes. All parsing
|
||||||
@@ -478,6 +483,13 @@ section directory with neither a dynamic symbol table nor a dynamic segment
|
|||||||
reports a complete empty inventory. Mach-O exports are reported by
|
reports a complete empty inventory. Mach-O exports are reported by
|
||||||
`inspect_macho`.
|
`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`).
|
`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
|
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
|
dynamic symbol table above 262144 entries, more than 262144 rows, or more than
|
||||||
|
|||||||
@@ -173,5 +173,12 @@ 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
|
with a resource error whose recovery is a smaller `limit`, because
|
||||||
`max_ranges` and `max_file_bytes` do not raise it.
|
`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
|
Malformed ELF headers fail without partial success. This workflow does not
|
||||||
execute the image, evaluate CFA programs, or follow indirect pointers.
|
execute the image, evaluate CFA programs, or follow indirect pointers.
|
||||||
@@ -150,6 +150,11 @@ 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
|
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.
|
`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
|
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.
|
observe cancellation, so a large `DT_RELR` table does not block the server.
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,6 @@ it("reads a Python 3.9 reference-flagged PYZ table", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reads marshal one-byte string tags as Latin-1", () => {
|
|
||||||
for (const hex of ["6101000000e9", "4101000000e9", "7a01e9", "5a01e9"])
|
|
||||||
expect(readMarshalSubset(bytes(hex))).toEqual({ ok: true, value: "é" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("decodes CPython marshal Unicode with surrogatepass", () => {
|
it("decodes CPython marshal Unicode with surrogatepass", () => {
|
||||||
expect(
|
expect(
|
||||||
readMarshalSubset(
|
readMarshalSubset(
|
||||||
|
|||||||
@@ -168,8 +168,14 @@ const readAsciiBytes = (
|
|||||||
): Result<string, PyInstallerFailure> => {
|
): Result<string, PyInstallerFailure> => {
|
||||||
const bytes = readBytes(cursor, length);
|
const bytes = readBytes(cursor, length);
|
||||||
if (!bytes.ok) return bytes;
|
if (!bytes.ok) return bytes;
|
||||||
// These tags use CPython's one-byte Unicode constructor, not an ASCII check.
|
if (bytes.value.some((byte) => byte > 0x7f))
|
||||||
return ok(bytes.value.toString("latin1"));
|
return err(
|
||||||
|
pyInstallerFailure(
|
||||||
|
"format",
|
||||||
|
"PYZ marshal ASCII string contains a non-ASCII byte",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return ok(bytes.value.toString("ascii"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const readUtf8 = (
|
const readUtf8 = (
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const sectionBytes = (
|
|||||||
section: NativeSection,
|
section: NativeSection,
|
||||||
budget?: CudaMetadataBudget,
|
budget?: CudaMetadataBudget,
|
||||||
): Promise<Buffer> => {
|
): Promise<Buffer> => {
|
||||||
if (!section.file_backed || (section.flags & 0x800n) !== 0n)
|
if (!section.file_backed || section.compressed)
|
||||||
rejectGpu("GPU metadata section has no supported uncompressed file range");
|
rejectGpu("GPU metadata section has no supported uncompressed file range");
|
||||||
budget?.read(gpuCount(section.size));
|
budget?.read(gpuCount(section.size));
|
||||||
return source.read(gpuCount(section.offset), gpuCount(section.size));
|
return source.read(gpuCount(section.offset), gpuCount(section.size));
|
||||||
|
|||||||
@@ -194,10 +194,7 @@ export class GpuInventory {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!isGpuSection(section.name)) continue;
|
if (!isGpuSection(section.name)) continue;
|
||||||
if (
|
if (!section.file_backed || section.compressed) {
|
||||||
!section.file_backed ||
|
|
||||||
(directory.format === "elf" && (section.flags & 0x800n) !== 0n)
|
|
||||||
) {
|
|
||||||
this.discovery = "partial";
|
this.discovery = "partial";
|
||||||
const message = `GPU section ${section.index} has no supported uncompressed file range`;
|
const message = `GPU section ${section.index} has no supported uncompressed file range`;
|
||||||
this.#retain(message);
|
this.#retain(message);
|
||||||
|
|||||||
@@ -340,11 +340,7 @@ export const inspectAndroidJniLibrary = async (
|
|||||||
for (const section of directory.sections) {
|
for (const section of directory.sections) {
|
||||||
if ((section.flags & 3n) !== 3n || section.size === 0n) continue;
|
if ((section.flags & 3n) !== 3n || section.size === 0n) continue;
|
||||||
active();
|
active();
|
||||||
if (
|
if (section.compressed || !section.file_backed || section.type === 8) {
|
||||||
(section.flags & 0x800n) !== 0n ||
|
|
||||||
!section.file_backed ||
|
|
||||||
section.type === 8
|
|
||||||
) {
|
|
||||||
tablesStatus = "partial";
|
tablesStatus = "partial";
|
||||||
unknowns.add(
|
unknowns.add(
|
||||||
"Allocated writable sections without ordinary file bytes were not scanned; runtime-built tables remain unknown",
|
"Allocated writable sections without ordinary file bytes were not scanned; runtime-built tables remain unknown",
|
||||||
|
|||||||
@@ -462,6 +462,33 @@ const findSection = (bytes: Buffer, name: string) => {
|
|||||||
throw new Error(`fixture has no ${name}`);
|
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", () => {
|
it("keeps .eh_frame ranges when .debug_frame is SHT_NOBITS", () => {
|
||||||
const report = parse(
|
const report = parse(
|
||||||
elfUnwindFixture({
|
elfUnwindFixture({
|
||||||
@@ -591,6 +618,18 @@ 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)(
|
it.each([".eh_frame", ".eh_frame_hdr"] as const)(
|
||||||
"reports coverage incomplete when a duplicate %s is ignored",
|
"reports coverage incomplete when a duplicate %s is ignored",
|
||||||
(duplicate) => {
|
(duplicate) => {
|
||||||
@@ -665,6 +704,22 @@ 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", () => {
|
it("caps .eh_frame_hdr rows at the max_ranges budget and marks the comparison incomplete", () => {
|
||||||
const report = parse(elfUnwindFixture({ header: "duplicate-row" }), {
|
const report = parse(elfUnwindFixture({ header: "duplicate-row" }), {
|
||||||
max_ranges: 2,
|
max_ranges: 2,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const AUG_R = 0x52;
|
|||||||
const AUG_S = 0x53;
|
const AUG_S = 0x53;
|
||||||
const AUG_B = 0x42;
|
const AUG_B = 0x42;
|
||||||
const SHT_SYMTAB = 2;
|
const SHT_SYMTAB = 2;
|
||||||
|
const SHT_STRTAB = 3;
|
||||||
const SHT_DYNAMIC = 6;
|
const SHT_DYNAMIC = 6;
|
||||||
const SHT_NOBITS = 8;
|
const SHT_NOBITS = 8;
|
||||||
const SHT_DYNSYM = 11;
|
const SHT_DYNSYM = 11;
|
||||||
@@ -49,7 +50,6 @@ const SHN_ABS = 0xfff1;
|
|||||||
const SHN_LORESERVE = 0xff00;
|
const SHN_LORESERVE = 0xff00;
|
||||||
const SHF_ALLOC = 2n;
|
const SHF_ALLOC = 2n;
|
||||||
const SHF_EXECINSTR = 4n;
|
const SHF_EXECINSTR = 4n;
|
||||||
const SHF_COMPRESSED = 0x800n;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
cfa: CFA_LIMITATION,
|
cfa: CFA_LIMITATION,
|
||||||
@@ -639,7 +639,7 @@ export const parseElfUnwindRanges = (
|
|||||||
? ".eh_frame_hdr is declared but not file-backed."
|
? ".eh_frame_hdr is declared but not file-backed."
|
||||||
: ".eh_frame_hdr extends past the end of the file and was not decoded.",
|
: ".eh_frame_hdr extends past the end of the file and was not decoded.",
|
||||||
);
|
);
|
||||||
} else if ((hdrSection.flags & SHF_COMPRESSED) !== 0n) {
|
} else if (hdrSection.compressed) {
|
||||||
headerLocation = placement.location;
|
headerLocation = placement.location;
|
||||||
headerStatus = "incomplete";
|
headerStatus = "incomplete";
|
||||||
fail(".eh_frame_hdr is SHF_COMPRESSED and was not decompressed.");
|
fail(".eh_frame_hdr is SHF_COMPRESSED and was not decompressed.");
|
||||||
@@ -705,7 +705,7 @@ export const parseElfUnwindRanges = (
|
|||||||
} else if (frameLocation === null) {
|
} else if (frameLocation === null) {
|
||||||
frameStatus = "incomplete";
|
frameStatus = "incomplete";
|
||||||
fail(".eh_frame lies outside the file.");
|
fail(".eh_frame lies outside the file.");
|
||||||
} else if ((ehSection.flags & SHF_COMPRESSED) !== 0n) {
|
} else if (ehSection.compressed) {
|
||||||
frameStatus = "incomplete";
|
frameStatus = "incomplete";
|
||||||
fail(".eh_frame is SHF_COMPRESSED and was not decompressed.");
|
fail(".eh_frame is SHF_COMPRESSED and was not decompressed.");
|
||||||
} else {
|
} else {
|
||||||
@@ -1135,7 +1135,12 @@ const resolveGotBase = (
|
|||||||
? undefined
|
? undefined
|
||||||
: { offset: dynamicProgram.offset, fileSize: dynamicProgram.size }) ??
|
: { offset: dynamicProgram.offset, fileSize: dynamicProgram.size }) ??
|
||||||
directory.sections
|
directory.sections
|
||||||
.filter((section) => section.type === SHT_DYNAMIC && section.file_backed)
|
.filter(
|
||||||
|
(section) =>
|
||||||
|
section.type === SHT_DYNAMIC &&
|
||||||
|
section.file_backed &&
|
||||||
|
!section.compressed,
|
||||||
|
)
|
||||||
.map((section) => ({
|
.map((section) => ({
|
||||||
offset: section.offset,
|
offset: section.offset,
|
||||||
fileSize: section.size,
|
fileSize: section.size,
|
||||||
@@ -1814,6 +1819,10 @@ const scanSymbols = (
|
|||||||
let remaining = MAX_ELF_UNWIND_SYMBOLS;
|
let remaining = MAX_ELF_UNWIND_SYMBOLS;
|
||||||
for (const table of tables) {
|
for (const table of tables) {
|
||||||
assertArtifactReadActive(signal);
|
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);
|
const width = table.entry_size === 0n ? standard : Number(table.entry_size);
|
||||||
if (width !== standard) {
|
if (width !== standard) {
|
||||||
fail(
|
fail(
|
||||||
@@ -1829,6 +1838,15 @@ const scanSymbols = (
|
|||||||
const size = Number(table.size);
|
const size = Number(table.size);
|
||||||
if (size % width !== 0)
|
if (size % width !== 0)
|
||||||
fail("A symbol table size is not a multiple of its entry width.");
|
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);
|
let count = Math.floor(size / width);
|
||||||
if (count > remaining) {
|
if (count > remaining) {
|
||||||
fail(
|
fail(
|
||||||
@@ -1837,15 +1855,10 @@ const scanSymbols = (
|
|||||||
count = remaining;
|
count = remaining;
|
||||||
}
|
}
|
||||||
remaining -= count;
|
remaining -= count;
|
||||||
const strings = byIndex.get(table.link);
|
const stringBytes = bytes.subarray(
|
||||||
const stringBytes =
|
Number(strings.offset),
|
||||||
strings !== undefined && strings.file_backed
|
Number(strings.offset + strings.size),
|
||||||
? 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.
|
// Observation per string-table offset; null when the name is unreadable.
|
||||||
const names = new Map<number, SymbolName | null>();
|
const names = new Map<number, SymbolName | null>();
|
||||||
for (let index = 0; index < count; index += 1) {
|
for (let index = 0; index < count; index += 1) {
|
||||||
@@ -1878,7 +1891,7 @@ const scanSymbols = (
|
|||||||
if (owner !== undefined) address += owner.address;
|
if (owner !== undefined) address += owner.address;
|
||||||
}
|
}
|
||||||
// Only a symbol at a decoded FDE start can name a range.
|
// Only a symbol at a decoded FDE start can name a range.
|
||||||
if (stringBytes === null || !wanted.has(address)) continue;
|
if (!wanted.has(address)) continue;
|
||||||
let name = names.get(symbol.name_offset);
|
let name = names.get(symbol.name_offset);
|
||||||
if (name === undefined) {
|
if (name === undefined) {
|
||||||
const observed = observeElfStringTableEntry(
|
const observed = observeElfStringTableEntry(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { deflateSync } from "node:zlib";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
elfExportImage,
|
elfExportImage,
|
||||||
@@ -22,6 +23,172 @@ const parse = (bytes: Buffer) =>
|
|||||||
parseNativeExports(bytes, artifact),
|
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)(
|
it.each(["requirements", "definitions"] as const)(
|
||||||
"bounds aliased GNU version %s work and keeps exports with unknown versions",
|
"bounds aliased GNU version %s work and keeps exports with unknown versions",
|
||||||
(kind) => {
|
(kind) => {
|
||||||
@@ -706,13 +873,12 @@ describe("ELF version requirements", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("uses a definition name when a requirement shares its index", () => {
|
it("uses a definition name when a requirement shares its index", () => {
|
||||||
const result = parse(
|
const bytes = elfExportImage({
|
||||||
elfExportImage({
|
symbols: [{ name: "ambiguous", version: 2 }],
|
||||||
symbols: [{ name: "ambiguous", version: 2 }],
|
versionDefinitions: ["libfixture.so", "DEFINITION"],
|
||||||
versionDefinitions: ["libfixture.so", "DEFINITION"],
|
versionRequirements: [{ index: 2, name: "REQUIREMENT" }],
|
||||||
versionRequirements: [{ index: 2, name: "REQUIREMENT" }],
|
});
|
||||||
}),
|
const result = parse(bytes);
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.exports).toMatchObject([
|
expect(result.exports).toMatchObject([
|
||||||
{
|
{
|
||||||
@@ -722,6 +888,10 @@ describe("ELF version requirements", () => {
|
|||||||
]);
|
]);
|
||||||
expect(result.coverage.status).toBe("complete");
|
expect(result.coverage.status).toBe("complete");
|
||||||
expect(result.issues).toEqual([]);
|
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", () => {
|
it("retains exports and reports a truncated version-requirement chain", () => {
|
||||||
|
|||||||
@@ -294,12 +294,22 @@ const versionSectionData = (
|
|||||||
table: { readonly offset: number; readonly size: number };
|
table: { readonly offset: number; readonly size: number };
|
||||||
names: Buffer;
|
names: Buffer;
|
||||||
} => {
|
} => {
|
||||||
|
if (section.compressed)
|
||||||
|
throw new BinaryFormatFailure(
|
||||||
|
"unsupported",
|
||||||
|
`${label} are SHF_COMPRESSED and were not decompressed`,
|
||||||
|
);
|
||||||
const strings = sections[section.link];
|
const strings = sections[section.link];
|
||||||
if (strings?.type !== SHT_STRTAB)
|
if (strings?.type !== SHT_STRTAB)
|
||||||
throw new BinaryFormatFailure(
|
throw new BinaryFormatFailure(
|
||||||
"malformed",
|
"malformed",
|
||||||
`${label} do not link to a string table`,
|
`${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(
|
const stringRange = reader.fileRange(
|
||||||
strings.offset,
|
strings.offset,
|
||||||
strings.size,
|
strings.size,
|
||||||
@@ -545,7 +555,13 @@ function* readVersionNames(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
if (cause instanceof ElfVersionWorkFailure) namesByIndex.clear();
|
// 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();
|
||||||
issues.push({
|
issues.push({
|
||||||
code: "version_unresolved",
|
code: "version_unresolved",
|
||||||
location: hex(verdef.offset),
|
location: hex(verdef.offset),
|
||||||
@@ -605,6 +621,32 @@ function* parseElfExports(
|
|||||||
};
|
};
|
||||||
const width = directory.bits === 64 ? 24 : 16;
|
const width = directory.bits === 64 ? 24 : 16;
|
||||||
const strings = sections[dynsym.link];
|
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 (
|
if (
|
||||||
dynsym.entry_size !== BigInt(width) ||
|
dynsym.entry_size !== BigInt(width) ||
|
||||||
dynsym.size % BigInt(width) !== 0n ||
|
dynsym.size % BigInt(width) !== 0n ||
|
||||||
@@ -647,6 +689,11 @@ function* parseElfExports(
|
|||||||
let versions: number | null = null;
|
let versions: number | null = null;
|
||||||
if (versym !== undefined) {
|
if (versym !== undefined) {
|
||||||
try {
|
try {
|
||||||
|
if (versym.compressed)
|
||||||
|
throw new BinaryFormatFailure(
|
||||||
|
"unsupported",
|
||||||
|
"ELF symbol versions are SHF_COMPRESSED and were not decompressed",
|
||||||
|
);
|
||||||
if (versym.size !== BigInt(count * 2))
|
if (versym.size !== BigInt(count * 2))
|
||||||
throw new BinaryFormatFailure(
|
throw new BinaryFormatFailure(
|
||||||
"malformed",
|
"malformed",
|
||||||
|
|||||||
@@ -1405,13 +1405,40 @@ it("sizes an empty DT_GNU_HASH table only from an agreeing SHT_DYNSYM", () => {
|
|||||||
strings.readBigUInt64LE(elfSectionAt(strings, SHT_STRTAB) + 32) - 1n,
|
strings.readBigUInt64LE(elfSectionAt(strings, SHT_STRTAB) + 32) - 1n,
|
||||||
elfSectionAt(strings, SHT_STRTAB) + 32,
|
elfSectionAt(strings, SHT_STRTAB) + 32,
|
||||||
);
|
);
|
||||||
for (const bytes of [moved, offset, strings])
|
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,
|
||||||
|
])
|
||||||
expect(
|
expect(
|
||||||
findRow(readElfHardening(bytes).rows, "stack_protector"),
|
findRow(readElfHardening(bytes).rows, "stack_protector"),
|
||||||
).toMatchObject({
|
).toMatchObject({
|
||||||
state: "unknown",
|
state: "unknown",
|
||||||
note: expect.stringContaining("DT_GNU_HASH"),
|
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", () => {
|
it("keeps an unresolved symbol count distinct from an absent table", () => {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
const ET_EXEC = 2;
|
const ET_EXEC = 2;
|
||||||
const ET_DYN = 3;
|
const ET_DYN = 3;
|
||||||
const SHT_SYMTAB = 2;
|
const SHT_SYMTAB = 2;
|
||||||
|
const SHT_STRTAB = 3;
|
||||||
const SHT_NOTE = 7;
|
const SHT_NOTE = 7;
|
||||||
const SHT_DYNSYM = 11;
|
const SHT_DYNSYM = 11;
|
||||||
const DT_NULL = 0n;
|
const DT_NULL = 0n;
|
||||||
@@ -71,7 +72,6 @@ const DF_1_PIE = 0x08000000n;
|
|||||||
const PF_X = 1;
|
const PF_X = 1;
|
||||||
const GNU_PROPERTY_AARCH64_FEATURE_1_AND = 0xc0000000;
|
const GNU_PROPERTY_AARCH64_FEATURE_1_AND = 0xc0000000;
|
||||||
const GNU_PROPERTY_X86_FEATURE_1_AND = 0xc0000002;
|
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. */
|
/** Tags hardening reads as a single value. Repeating tags such as DT_NEEDED stay. */
|
||||||
const SINGULAR_DYNAMIC_TAGS = new Set<bigint>([
|
const SINGULAR_DYNAMIC_TAGS = new Set<bigint>([
|
||||||
DT_HASH,
|
DT_HASH,
|
||||||
@@ -175,7 +175,6 @@ export const readElfHardening = (
|
|||||||
assertElfDeclaredRangesFromBuffer(bytes);
|
assertElfDeclaredRangesFromBuffer(bytes);
|
||||||
const { bits, little, machine, programs, sections } = directory;
|
const { bits, little, machine, programs, sections } = directory;
|
||||||
const headerWidth = bits === 64 ? 56 : 32;
|
const headerWidth = bits === 64 ? 56 : 32;
|
||||||
const symbolWidth = bits === 64 ? 24 : 16;
|
|
||||||
const dynamicWidth = bits === 64 ? 16 : 8;
|
const dynamicWidth = bits === 64 ? 16 : 8;
|
||||||
const propertyAlign = bits === 64 ? 8 : 4;
|
const propertyAlign = bits === 64 ? 8 : 4;
|
||||||
const loads = programs.filter(isSpan).filter((program) => program.type === 1);
|
const loads = programs.filter(isSpan).filter((program) => program.type === 1);
|
||||||
@@ -546,10 +545,14 @@ const agreeingSectionCount = (
|
|||||||
if (dynsym === undefined) return null;
|
if (dynsym === undefined) return null;
|
||||||
const strings = sections[dynsym.link];
|
const strings = sections[dynsym.link];
|
||||||
if (
|
if (
|
||||||
|
dynsym.compressed ||
|
||||||
|
strings === undefined ||
|
||||||
|
strings.compressed ||
|
||||||
|
strings.type !== SHT_STRTAB ||
|
||||||
dynsym.address !== symtab.value ||
|
dynsym.address !== symtab.value ||
|
||||||
strings?.address !== dynamic.entries.get(DT_STRTAB)?.value ||
|
strings.address !== dynamic.entries.get(DT_STRTAB)?.value ||
|
||||||
strings?.size !== dynamic.entries.get(DT_STRSZ)?.value ||
|
strings.size !== dynamic.entries.get(DT_STRSZ)?.value ||
|
||||||
strings?.offset !== BigInt(dynamic.stringFileOffset ?? -1)
|
strings.offset !== BigInt(dynamic.stringFileOffset ?? -1)
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
const entry = dynsym.entry_size === 0n ? BigInt(width) : dynsym.entry_size;
|
const entry = dynsym.entry_size === 0n ? BigInt(width) : dynsym.entry_size;
|
||||||
@@ -897,11 +900,10 @@ const readFeatureNotes = (
|
|||||||
if (section.type !== SHT_NOTE) continue;
|
if (section.type !== SHT_NOTE) continue;
|
||||||
// An empty note section owns no bytes, so it has nothing to walk.
|
// An empty note section owns no bytes, so it has nothing to walk.
|
||||||
if (section.size === 0n) continue;
|
if (section.size === 0n) continue;
|
||||||
const compressed = (section.flags & SHF_COMPRESSED) !== 0n;
|
if (section.compressed)
|
||||||
if (compressed)
|
|
||||||
incomplete ??=
|
incomplete ??=
|
||||||
"A compressed SHT_NOTE section may hold GNU property notes, and its descriptors were not read.";
|
"A compressed SHT_NOTE section may hold GNU property notes, and its descriptors were not read.";
|
||||||
if (compressed) {
|
if (section.compressed) {
|
||||||
if (!section.file_backed) continue;
|
if (!section.file_backed) continue;
|
||||||
otherSpans.push({
|
otherSpans.push({
|
||||||
offset: exact(section.offset, "SHT_NOTE offset"),
|
offset: exact(section.offset, "SHT_NOTE offset"),
|
||||||
|
|||||||
@@ -124,11 +124,17 @@ const stringAt = (
|
|||||||
return { name: text, missing: false };
|
return { name: text, missing: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableBytes = (
|
const stringTableBytes = (
|
||||||
bytes: Buffer,
|
bytes: Buffer,
|
||||||
section: NativeSection | undefined,
|
section: NativeSection | undefined,
|
||||||
): Buffer | null => {
|
): Buffer | null => {
|
||||||
if (section === undefined || !section.file_backed) return null;
|
if (
|
||||||
|
section === undefined ||
|
||||||
|
section.type !== 3 ||
|
||||||
|
!section.file_backed ||
|
||||||
|
section.compressed
|
||||||
|
)
|
||||||
|
return null;
|
||||||
const offset = Number(section.offset);
|
const offset = Number(section.offset);
|
||||||
const size = Number(section.size);
|
const size = Number(section.size);
|
||||||
if (offset < 0 || size < 0 || offset > bytes.length - size) return null;
|
if (offset < 0 || size < 0 || offset > bytes.length - size) return null;
|
||||||
@@ -144,9 +150,19 @@ const readTable = (
|
|||||||
): {
|
): {
|
||||||
readonly symbols: ElfInitializerSymbol[];
|
readonly symbols: ElfInitializerSymbol[];
|
||||||
readonly truncated: boolean;
|
readonly truncated: boolean;
|
||||||
|
readonly incomplete: boolean;
|
||||||
} => {
|
} => {
|
||||||
const width = facts.bits === 64 ? 24 : 16;
|
const width = facts.bits === 64 ? 24 : 16;
|
||||||
const strings = tableBytes(facts.bytes, sections[section.link]);
|
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 start = Number(section.offset);
|
const start = Number(section.offset);
|
||||||
const declared = section.size / BigInt(width);
|
const declared = section.size / BigInt(width);
|
||||||
const count = Number(declared < BigInt(budget) ? declared : BigInt(budget));
|
const count = Number(declared < BigInt(budget) ? declared : BigInt(budget));
|
||||||
@@ -177,7 +193,7 @@ const readTable = (
|
|||||||
type: parsed.value.type,
|
type: parsed.value.type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { symbols, truncated: truncated || nameMissing };
|
return { symbols, truncated, incomplete: nameMissing || strings === null };
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Load SHT_SYMTAB and SHT_DYNSYM once. Defined STT_FUNC names win ties. */
|
/** Load SHT_SYMTAB and SHT_DYNSYM once. Defined STT_FUNC names win ties. */
|
||||||
@@ -223,10 +239,20 @@ export const loadElfSymbolIndex = (
|
|||||||
sections.forEach((section, index) => {
|
sections.forEach((section, index) => {
|
||||||
if (truncated) return;
|
if (truncated) return;
|
||||||
if (section.type !== 2 && section.type !== 11) return;
|
if (section.type !== 2 && section.type !== 11) return;
|
||||||
if (
|
if (section.compressed) {
|
||||||
!section.file_backed ||
|
notes.add("A symbol table is compressed; its symbols were not decoded.");
|
||||||
section.size % BigInt(facts.bits === 64 ? 24 : 16) !== 0n
|
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) {
|
||||||
notes.add(
|
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.",
|
"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.",
|
||||||
);
|
);
|
||||||
@@ -237,6 +263,7 @@ export const loadElfSymbolIndex = (
|
|||||||
budget -= table.symbols.length;
|
budget -= table.symbols.length;
|
||||||
tables.set(index, table.symbols);
|
tables.set(index, table.symbols);
|
||||||
truncated ||= table.truncated;
|
truncated ||= table.truncated;
|
||||||
|
unreadable ||= table.incomplete;
|
||||||
for (const symbol of table.symbols) consider(symbol);
|
for (const symbol of table.symbols) consider(symbol);
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -440,7 +467,11 @@ export const sectionRelocations = async (
|
|||||||
for (const [position, section] of input.sections.entries()) {
|
for (const [position, section] of input.sections.entries()) {
|
||||||
const encoding = applicable(input, section);
|
const encoding = applicable(input, section);
|
||||||
if (encoding === null) continue;
|
if (encoding === null) continue;
|
||||||
if (!section.file_backed) {
|
if (!section.file_backed || section.compressed) {
|
||||||
|
if (section.compressed)
|
||||||
|
input.notes.add(
|
||||||
|
"A relocation section is compressed; its records were not decoded.",
|
||||||
|
);
|
||||||
skip(section);
|
skip(section);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ const indexSections = (sections: readonly NativeSection[]): ElfSectionIndex => {
|
|||||||
: left.index - right.index,
|
: left.index - right.index,
|
||||||
);
|
);
|
||||||
const backed = buildAddressRangeIndex(
|
const backed = buildAddressRangeIndex(
|
||||||
ranges((section) => section.file_backed),
|
ranges((section) => section.file_backed && !section.compressed),
|
||||||
(left, right) => left.index - right.index,
|
(left, right) => left.index - right.index,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -354,7 +354,8 @@ const findSections = (
|
|||||||
const withinSection =
|
const withinSection =
|
||||||
(section: NativeSection, width: number) =>
|
(section: NativeSection, width: number) =>
|
||||||
(address: bigint): number | null => {
|
(address: bigint): number | null => {
|
||||||
if (!section.file_backed || address < section.address) return null;
|
if (!section.file_backed || section.compressed || address < section.address)
|
||||||
|
return null;
|
||||||
const delta = address - section.address;
|
const delta = address - section.address;
|
||||||
if (delta + BigInt(width) > section.size) return null;
|
if (delta + BigInt(width) > section.size) return null;
|
||||||
return Number(section.offset + delta);
|
return Number(section.offset + delta);
|
||||||
@@ -374,6 +375,13 @@ const sectionSource = (
|
|||||||
section.name.length <= MAX_NATIVE_INITIALIZER_NAME_BYTES
|
section.name.length <= MAX_NATIVE_INITIALIZER_NAME_BYTES
|
||||||
? section.name
|
? section.name
|
||||||
: fallbackName;
|
: 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) {
|
if (!section.file_backed) {
|
||||||
note(scan, `${label} is not file-backed. Its slots were omitted.`);
|
note(scan, `${label} is not file-backed. Its slots were omitted.`);
|
||||||
return { kind, sentinel, detail: label, slots: [] };
|
return { kind, sentinel, detail: label, slots: [] };
|
||||||
@@ -671,6 +679,7 @@ const loadWrites = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const dynamicSymbols = (
|
const dynamicSymbols = (
|
||||||
|
scan: ElfScan,
|
||||||
sections: readonly NativeSection[],
|
sections: readonly NativeSection[],
|
||||||
tags: ReadonlyMap<bigint, DynTag>,
|
tags: ReadonlyMap<bigint, DynTag>,
|
||||||
tables: ReadonlyMap<number, readonly ElfInitializerSymbol[]>,
|
tables: ReadonlyMap<number, readonly ElfInitializerSymbol[]>,
|
||||||
@@ -683,6 +692,11 @@ const dynamicSymbols = (
|
|||||||
(section.type === 11 || section.type === 2),
|
(section.type === 11 || section.type === 2),
|
||||||
);
|
);
|
||||||
if (match >= 0) return tables.get(match);
|
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);
|
const dynsym = sections.findIndex((section) => section.type === 11);
|
||||||
return dynsym < 0 ? undefined : tables.get(dynsym);
|
return dynsym < 0 ? undefined : tables.get(dynsym);
|
||||||
@@ -1042,7 +1056,7 @@ export const readElfInitializers = async (
|
|||||||
loaded,
|
loaded,
|
||||||
sectionRels.found,
|
sectionRels.found,
|
||||||
sectionRels.clobbered,
|
sectionRels.clobbered,
|
||||||
dynamicSymbols(directory.sections, tags, symbols.tables),
|
dynamicSymbols(scan, directory.sections, tags, symbols.tables),
|
||||||
symbols,
|
symbols,
|
||||||
loaded.trustRaw && sectionRels.complete,
|
loaded.trustRaw && sectionRels.complete,
|
||||||
directory.sections,
|
directory.sections,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { deflateSync } from "node:zlib";
|
||||||
import { readNativeInitializers } from "./NativeInitializersProvider.js";
|
import { readNativeInitializers } from "./NativeInitializersProvider.js";
|
||||||
|
|
||||||
/** Pass-through spies on the shared mapping module, so tests can count lookups made from other modules. */
|
/** Pass-through spies on the shared mapping module, so tests can count lookups made from other modules. */
|
||||||
@@ -320,6 +321,106 @@ 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", () => {
|
describe("ELF incomplete inputs", () => {
|
||||||
it("reports an unsupported relocation machine as incomplete, not as raw zeros", async () => {
|
it("reports an unsupported relocation machine as incomplete, not as raw zeros", async () => {
|
||||||
const bytes = elf64PieInitializers();
|
const bytes = elf64PieInitializers();
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ export const readElfImage = (
|
|||||||
}
|
}
|
||||||
for (const section of directory.sections) {
|
for (const section of directory.sections) {
|
||||||
const range = declaredFileRange(bytes.length, section.offset, section.size);
|
const range = declaredFileRange(bytes.length, section.offset, section.size);
|
||||||
const file_backed = section.type !== 8 && range.file_backed;
|
const { file_backed, compressed } = section;
|
||||||
const compressed = (section.flags & 0x800n) !== 0n;
|
|
||||||
const read_only =
|
const read_only =
|
||||||
section.type === 1 &&
|
section.type === 1 &&
|
||||||
file_backed &&
|
file_backed &&
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { expect, it } from "vitest";
|
|||||||
import { pe } from "../../domain/binaryTarget.fixture.js";
|
import { pe } from "../../domain/binaryTarget.fixture.js";
|
||||||
import { elfHardeningFixture } from "../hardening/BinaryHardening.fixture.js";
|
import { elfHardeningFixture } from "../hardening/BinaryHardening.fixture.js";
|
||||||
import { elfUnwindSectionlessFixture } from "../elf/ElfUnwindTables.fixture.js";
|
import { elfUnwindSectionlessFixture } from "../elf/ElfUnwindTables.fixture.js";
|
||||||
|
import { compressedElfNoteSectionFixture } from "../elf/ElfNotes.fixture.js";
|
||||||
import {
|
import {
|
||||||
ELF_PT_GNU_EH_FRAME,
|
ELF_PT_GNU_EH_FRAME,
|
||||||
ELF_PT_GNU_RELRO,
|
ELF_PT_GNU_RELRO,
|
||||||
@@ -94,3 +95,35 @@ 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,7 +45,10 @@ export interface NativeSection {
|
|||||||
readonly entry_size: bigint;
|
readonly entry_size: bigint;
|
||||||
/** `sh_addralign` for ELF. PE sections have no note alignment and use 1. */
|
/** `sh_addralign` for ELF. PE sections have no note alignment and use 1. */
|
||||||
readonly alignment: bigint;
|
readonly alignment: bigint;
|
||||||
|
/** Declared stored bytes are present; compressed bytes are not decoded content. */
|
||||||
readonly file_backed: boolean;
|
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. */
|
/** Program header types retained for load, dynamic, hardening, and unwind consumers. */
|
||||||
export const ELF_PT_NOTE = 4 as const;
|
export const ELF_PT_NOTE = 4 as const;
|
||||||
@@ -308,6 +311,7 @@ export function* readElfSectionDirectory(
|
|||||||
alignment: section.alignment,
|
alignment: section.alignment,
|
||||||
file_backed:
|
file_backed:
|
||||||
type !== 8 && declaredFileRange(fileSize, offset, size).file_backed,
|
type !== 8 && declaredFileRange(fileSize, offset, size).file_backed,
|
||||||
|
compressed: (section.flags & 0x800n) !== 0n,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,6 +414,7 @@ export function* readPeSectionDirectory(
|
|||||||
BigInt(row.offset),
|
BigInt(row.offset),
|
||||||
BigInt(row.size),
|
BigInt(row.size),
|
||||||
).file_backed,
|
).file_backed,
|
||||||
|
compressed: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -407,30 +407,6 @@ it("charges long digits to the string budget and keeps large previews bounded",
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reads marshal one-byte string tags as Latin-1 with Unicode identity", () => {
|
|
||||||
for (const tag of [0x61, 0x41, 0x7a, 0x5a]) {
|
|
||||||
const encoded = Buffer.concat([
|
|
||||||
Buffer.from([tag]),
|
|
||||||
tag === 0x61 || tag === 0x41 ? u32(1) : Buffer.from([1]),
|
|
||||||
Buffer.from([0xe9]),
|
|
||||||
]);
|
|
||||||
const sameUnicode = marshalUnicode(Buffer.from("é", "utf8"));
|
|
||||||
const set = Buffer.concat([
|
|
||||||
Buffer.from([0x3c]),
|
|
||||||
u32(2),
|
|
||||||
encoded,
|
|
||||||
sameUnicode,
|
|
||||||
]);
|
|
||||||
const report = root(parse(withConsts([encoded, set])));
|
|
||||||
expect(report.consts[0]).toMatchObject({
|
|
||||||
kind: "str",
|
|
||||||
value: "é",
|
|
||||||
bytes: 1,
|
|
||||||
});
|
|
||||||
expect(report.consts[1]).toEqual({ kind: "set", length: 1 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("decodes marshal strings with CPython surrogatepass rules", () => {
|
it("decodes marshal strings with CPython surrogatepass rules", () => {
|
||||||
const report = parse(
|
const report = parse(
|
||||||
withConsts([marshalUnicode(Buffer.from("efbbbf73eda080efbbbf", "hex"))]),
|
withConsts([marshalUnicode(Buffer.from("efbbbf73eda080efbbbf", "hex"))]),
|
||||||
@@ -1558,36 +1534,6 @@ it("hashes a long TYPE_REF tuple chain without recursing", () => {
|
|||||||
expect(pythonPycSchema.safeParse(report).success).toBe(true);
|
expect(pythonPycSchema.safeParse(report).success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hashes shallow TYPE_REF code chains without using the JavaScript stack", () => {
|
|
||||||
const links = 3000;
|
|
||||||
const codes = Array.from({ length: links }, (_, index) =>
|
|
||||||
marshalCode("py311", {
|
|
||||||
flagged: true,
|
|
||||||
name: "f",
|
|
||||||
consts: index === 0 ? [Buffer.from([0x4e])] : [marshalRef(index - 1)],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const bytes = Buffer.concat([
|
|
||||||
pythonPycHeader(3531),
|
|
||||||
marshalCode("py311", {
|
|
||||||
constsObject: Buffer.concat([
|
|
||||||
Buffer.from([0x28]),
|
|
||||||
u32(links + 1),
|
|
||||||
...codes,
|
|
||||||
Buffer.from([0x3c]),
|
|
||||||
u32(1),
|
|
||||||
marshalRef(links - 1),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
const report = parseWith(bytes, { max_objects: 65_536 });
|
|
||||||
expect(root(report).consts.at(-1)).toEqual({ kind: "set", length: 1 });
|
|
||||||
expect(report.coverage).toMatchObject({
|
|
||||||
status: "complete",
|
|
||||||
code_objects: links + 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects code CPython treats as corrupt instead of complete coverage", () => {
|
it("rejects code CPython treats as corrupt instead of complete coverage", () => {
|
||||||
const file = (
|
const file = (
|
||||||
layout: "py37" | "py38" | "py311",
|
layout: "py37" | "py38" | "py311",
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ class MarshalReader {
|
|||||||
readonly #codeKeys = new WeakMap<CodeNode, string>();
|
readonly #codeKeys = new WeakMap<CodeNode, string>();
|
||||||
readonly #codeIdentities = new WeakMap<CodeNode, number>();
|
readonly #codeIdentities = new WeakMap<CodeNode, number>();
|
||||||
readonly #codeEquality = new WeakMap<CodeNode, CodeEquality>();
|
readonly #codeEquality = new WeakMap<CodeNode, CodeEquality>();
|
||||||
/** Completed Python hashability checks per code object. */
|
/** Python hashability per code object. False while that check is in progress. */
|
||||||
readonly #codeHashable = new WeakMap<CodeNode, boolean>();
|
readonly #codeHashable = new WeakMap<CodeNode, boolean>();
|
||||||
/** Tuples whose items are still being read; CPython cannot hash them yet. */
|
/** Tuples whose items are still being read; CPython cannot hash them yet. */
|
||||||
readonly #readingTuples = new WeakSet<MarshalValue>();
|
readonly #readingTuples = new WeakSet<MarshalValue>();
|
||||||
@@ -436,19 +436,13 @@ class MarshalReader {
|
|||||||
};
|
};
|
||||||
case TYPE_INTERNED:
|
case TYPE_INTERNED:
|
||||||
case TYPE_UNICODE:
|
case TYPE_UNICODE:
|
||||||
return this.string(
|
return this.utf8(this.nonNegativeInt32("Marshal string length"), false);
|
||||||
this.nonNegativeInt32("Marshal string length"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
case TYPE_ASCII:
|
case TYPE_ASCII:
|
||||||
case TYPE_ASCII_INTERNED:
|
case TYPE_ASCII_INTERNED:
|
||||||
return this.string(
|
return this.utf8(this.nonNegativeInt32("Marshal string length"), true);
|
||||||
this.nonNegativeInt32("Marshal string length"),
|
|
||||||
"latin1",
|
|
||||||
);
|
|
||||||
case TYPE_SHORT_ASCII:
|
case TYPE_SHORT_ASCII:
|
||||||
case TYPE_SHORT_ASCII_INTERNED:
|
case TYPE_SHORT_ASCII_INTERNED:
|
||||||
return this.string(this.u8(), "latin1");
|
return this.utf8(this.u8(), true);
|
||||||
case TYPE_TUPLE:
|
case TYPE_TUPLE:
|
||||||
return this.sequenceValue(
|
return this.sequenceValue(
|
||||||
"tuple",
|
"tuple",
|
||||||
@@ -1315,70 +1309,13 @@ class MarshalReader {
|
|||||||
#isCodeHashable(node: CodeNode): boolean {
|
#isCodeHashable(node: CodeNode): boolean {
|
||||||
const known = this.#codeHashable.get(node);
|
const known = this.#codeHashable.get(node);
|
||||||
if (known !== undefined) return known;
|
if (known !== undefined) return known;
|
||||||
const frames: { value: MarshalValue; finish: boolean }[] = [
|
this.#codeHashable.set(node, false);
|
||||||
{ value: { kind: "code", value: node }, finish: false },
|
const material =
|
||||||
];
|
this.#codeEquality.get(node) ??
|
||||||
const gray = new Set<MarshalValue | CodeNode>();
|
malformed("Code object is missing equality material.");
|
||||||
const complete = new Set<MarshalValue>();
|
const hashable = this.#equalityKey(material.constsTuple, "hash") !== null;
|
||||||
const reject = (): false => {
|
this.#codeHashable.set(node, hashable);
|
||||||
this.#codeHashable.set(node, false);
|
return hashable;
|
||||||
return false;
|
|
||||||
};
|
|
||||||
while (frames.length > 0) {
|
|
||||||
this.#takeKeyStep();
|
|
||||||
const frame = frames.pop()!;
|
|
||||||
const value = frame.value;
|
|
||||||
const identity = value.kind === "code" ? value.value : value;
|
|
||||||
if (frame.finish) {
|
|
||||||
gray.delete(identity);
|
|
||||||
if (value.kind === "code") {
|
|
||||||
const material =
|
|
||||||
this.#codeEquality.get(value.value) ??
|
|
||||||
malformed("Code object is missing equality material.");
|
|
||||||
// Child code checks are now complete, so the existing tuple key
|
|
||||||
// walker preserves its equality and expansion budgets without recursion.
|
|
||||||
const hashable =
|
|
||||||
this.#equalityKey(material.constsTuple, "hash") !== null;
|
|
||||||
this.#codeHashable.set(value.value, hashable);
|
|
||||||
if (!hashable) return reject();
|
|
||||||
} else complete.add(value);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (value.kind === "code") {
|
|
||||||
const cached = this.#codeHashable.get(value.value);
|
|
||||||
if (cached === false) return reject();
|
|
||||||
if (cached === true) continue;
|
|
||||||
} else {
|
|
||||||
const cached = this.#keyCache.get(value);
|
|
||||||
if (cached === null) return reject();
|
|
||||||
if (cached !== undefined || complete.has(value)) continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
gray.has(identity) ||
|
|
||||||
this.#readingTuples.has(value) ||
|
|
||||||
value.kind === "list" ||
|
|
||||||
value.kind === "set" ||
|
|
||||||
value.kind === "dict" ||
|
|
||||||
value.kind === "null"
|
|
||||||
)
|
|
||||||
return reject();
|
|
||||||
const children =
|
|
||||||
value.kind === "code"
|
|
||||||
? [
|
|
||||||
(
|
|
||||||
this.#codeEquality.get(value.value) ??
|
|
||||||
malformed("Code object is missing equality material.")
|
|
||||||
).constsTuple,
|
|
||||||
]
|
|
||||||
: value.kind === "tuple" || value.kind === "frozenset"
|
|
||||||
? value.items
|
|
||||||
: [];
|
|
||||||
gray.add(identity);
|
|
||||||
frames.push({ value, finish: true });
|
|
||||||
for (let index = children.length - 1; index >= 0; index -= 1)
|
|
||||||
frames.push({ value: children[index]!, finish: false });
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#codeIdentity(node: CodeNode): number {
|
#codeIdentity(node: CodeNode): number {
|
||||||
@@ -1555,20 +1492,17 @@ class MarshalReader {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
string(length: number, encoding: "latin1" | "utf8"): MarshalValue {
|
utf8(length: number, ascii: boolean): MarshalValue {
|
||||||
const bytes = this.blob(length);
|
const bytes = this.blob(length);
|
||||||
if (encoding === "latin1") {
|
if (ascii) {
|
||||||
// CPython reads the ASCII-tagged forms as one-byte Unicode, even when
|
if (bytes.some((byte) => byte > 0x7f))
|
||||||
// high bytes appear. Identity must agree with the UTF-8 string form.
|
malformed("Marshal ASCII string contains a non-ASCII byte.");
|
||||||
const value = bytes.toString("latin1");
|
|
||||||
return {
|
return {
|
||||||
kind: "str",
|
kind: "str",
|
||||||
value,
|
value: bytes.toString("latin1"),
|
||||||
previewEnd: Math.min(length, PYTHON_PYC_CONST_PREVIEW_LIMIT),
|
previewEnd: Math.min(length, PYTHON_PYC_CONST_PREVIEW_LIMIT),
|
||||||
bytes: length,
|
bytes: length,
|
||||||
raw: bytes.some((byte) => byte > 0x7f)
|
raw: bytes,
|
||||||
? Buffer.from(value, "utf8")
|
|
||||||
: bytes,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const decoded = decodeMarshalUtf8(bytes, PYTHON_PYC_CONST_PREVIEW_LIMIT);
|
const decoded = decodeMarshalUtf8(bytes, PYTHON_PYC_CONST_PREVIEW_LIMIT);
|
||||||
|
|||||||
Reference in new issue
Block a user