Skip to content

Memory - #6

Merged
SecSamDev merged 9 commits into
v14from
memory
Oct 25, 2026
Merged

SecSamDev merged 9 commits into
v14from
memory

Conversation

@SecSamDev

Copy link
Copy Markdown
Collaborator

No description provided.

forensic-rs now defines what a memory backend provides and what analyzers,
parsers and tools consume, so code written against it works on any backend.

- src/memory/: Va/Pa, PageSize, Validity/Backing/ReadReport, PhysicalMemory,
  AddressSpace (+ AddressSpaceExt typed reads), Translation, typed records
  (MemOsInfo, MemProcess, MemThread, MemModule, MemRegion, MemHandle) with
  MemValue fields and MemRecordMeta provenance, MemoryImage (+ Ext), MemorySpec.
- Integration: Mounted::Memory/MountKind::Memory, Requirement::Memory,
  TriageSourcesBuilder::memory, CommonArtifact::Memory(MemoryArtifact),
  Locus::Memory/Physical, LocatorSegment::AddressSpace,
  AnomalyFlags::INFERRED_LAYOUT, memory EntityKinds and EntityId constructors,
  process.*/dll.*/memory.* dictionary fields.
- Test doubles: InMemoryPhysicalMemory, TestingAddressSpace,
  TestingMemoryImage, TestingFormatFactory::memory.
- tests/memory_contract.rs: a backend-independent analyzer and parser factory.
read_bytes allocated vec![0; len] for any len, and lengths in memory
images are attacker-controlled: a hostile size was an allocation DoS for
every consumer. It now refuses lengths above MAX_READ (16 MiB) with a
too_big error; larger spans go through AddressSpace::read into a
caller-owned buffer.

read_cstr, read_utf16 and read_utf16_counted returned a plain Known
value when the bound cut the string. They now return Bounded<T>, whose
truncated flag says so.
A MemoryImage returns an error for a record kind it does not provide,
so that 'no handles' and 'handles not supported' never look alike. That
error was ForensicError::other, which callers could not tell from a
failed read.

DataAccessError::Unsupported, with ForensicError::unsupported and
is_unsupported (looking through context wrappers), names it.
memory::unsupported(kind) uses it.
Records' provenance ids were minted in the backend's store, which a
consumer of &dyn MemoryImage could not reach, so the ids were opaque.
MemoryImage::provenance() now returns that store.

MemRecordMeta::new defaulted validity to Resident, so a backend that
forgot to compute it claimed a full read. The validity is now a
required argument. MemOsInfo.live duplicated PhysicalMemory::is_live
with a default of false; it is removed, and is_live is the one place
liveness is stated.

TestingMemoryImage holds a store (with_provenance) and the contract
test resolves each record's provenance through the image.
MemRecordKind::Module stood for both kernel_modules() and
modules(process), so a backend providing one claimed both and the other
looked like an empty list. It is split into KernelModule and
ProcessModule.

os() was required although OsInfo is listed as an optional kind. It now
defaults to an unsupported error like every other kind.
A page whose data lives in a mapped file (on Windows, a prototype PTE
pointing at a subsection) had no translation of its own: backends could
only report it as an unresolved prototype, losing which file and where.
Translation::FileBacked { file, offset } carries an opaque file key and
the byte offset; unresolved, it reads as Paged { MappedFile, data: false }.
A Translation::PageFile page could only be reported as Paged { data:
false }: nothing let an investigator supply pagefile.sys or swapfile.sys.
PagingFile is one such file read at the offsets translations carry, and
PagingFiles holds them by the OS's paging-file number. Reads past the end
of a truncated copy, or of a number nobody attached, are zero-filled and
reported without data. InMemoryPagingFile is the test double.
Both decoders panicked on attacker-controlled data, which memory images,
hibernation files and prefetch all are.

lz77 indexed past the input on a cut literal, flags word, match or
length, underflowed on a back-reference before the output, and had no
output bound, so the padding after a memory-compression page ran it off
the end. It is rewritten with checked reads, back-references limited to
this call's output, and decompress_bounded(in, out, max_out).

xpress_huff read the bit stream header without checking it (inputs of
256 to 259 bytes), ran out of tree nodes on an over-subscribed table,
underflowed on a bad back-reference or an overshooting match. These are
errors now, and symbol decoding is bounded by the 15-bit code length.

Tests cover each case and fail on the old code; a decompress fuzz target
covers LZ77, Xpress Huffman and LZNT1.
Code walking lists over any AddressSpace stops at a link that is not a
canonical address, and could only assume 48 bits for 8-byte pointers. A
5-level paging (LA57) space implements 57 bits, so kernel pointers above
bit 47 looked like corrupt links.

AddressSpace::va_bits says how many bits a space implements; it defaults
to 48 for 8-byte pointers and 32 otherwise, so existing backends keep
their behaviour. Va::is_canonical and Va::is_upper_half check an address
against that width.
@SecSamDev
SecSamDev merged commit fac246b into v14 Oct 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant