Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions cle/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@

log = logging.getLogger(name=__name__)

# The floor for the guard region at the bottom of the address space. The loader hands it out only
# when there is nowhere else, because a null or uninitialized pointer in the target reads as an
# address inside it. Where the loader knows the target's page size, the guard tracks it instead.
NULL_PAGE_SIZE = 0x1000

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be based on the actual page size instead of hardcoded?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

There is no actual page size to read here, so the constant is now a floor rather than the answer. What I checked:

  • No archinfo Arch defines a page-size attribute, on any architecture — surveyed AMD64, X86, ARM, AArch64, MIPS32, PPC64, S390X, RISCV64 and ArchPcode (including the 24-bit avr8:LE:24:xmega); none carries any page-related attribute.
  • The one page-size datum cle holds is Loader.page_size, which defaults to 0x1 — "the granularity with which data is mapped into memory", i.e. target page size unknown. It carries a real page size when a core dump's mappings note supplies one (elfcore.py sets self.loader.page_size from the parsed note) or when the caller states one. It is assigned in Loader.__init__ before any object is mapped, so it is available where _find_safe_rebase_addr runs.

So as of d160d97 the guard is max(self.page_size, NULL_PAGE_SIZE): one target page where the loader knows the page size, and the conventional 4 KB floor where it does not — max(1, 0x1000) leaves every existing configuration unchanged. A load stating page_size=0x2000 now keeps both pages a null pointer could land in out of the placement; test_rebase.py::test_null_guard_tracks_the_loader_page_size pins that (it places the object at 0x1000 without the change and at 0x2000 with it). Full cle suite: 243 passed, 9 skipped.


if TYPE_CHECKING:
from .backends import Region, Section, Segment
from .backends.relocation import Relocation
Expand Down Expand Up @@ -1074,23 +1079,27 @@ def _find_safe_rebase_addr(self, size):
"""
# this assumes that self.main_object exists, which should... definitely be safe
limit = 2**self.main_object.arch.bits
above_image = self.main_object.max_addr + 1
if self.main_object.arch.bits < 32 or self.main_object.max_addr >= 2 ** (self.main_object.arch.bits - 1):
# HACK: On small arches, we should be more aggressive in packing stuff in. An image
# reaching into the top half of the address space leaves its free space underneath it.
start = 0
# A small address space, or an image that reaches into the top half of one, may have
# its free space underneath the image. Take that space once the space above the image
# is exhausted, and the null page only when there is nothing else at all. The
# loader's page_size defaults to 1, which means the target's page size is unknown.
starts = (above_image, max(self.page_size, NULL_PAGE_SIZE), 0)
else:
start = self.main_object.max_addr + 1
starts = (above_image,)

# The granularity is a preference, not a constraint: it costs up to one granule per object,
# which a small address space runs out of long before the space itself is full.
alignments = [self._rebase_granularity]
alignments += [a for a in (0x1000, 1) if a < self._rebase_granularity]

for alignment in alignments:
for gap_start, gap_end in self._free_gaps(start, limit):
addr = ALIGN_UP(gap_start, alignment)
if addr + size <= gap_end:
return addr
for start in starts:
for alignment in alignments:
for gap_start, gap_end in self._free_gaps(start, limit):
addr = ALIGN_UP(gap_start, alignment)
if addr + size <= gap_end:
return addr

raise CLEOperationError("Ran out of room in address space")

Expand Down
96 changes: 94 additions & 2 deletions tests/test_rebase.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from __future__ import annotations

import os
import unittest

import archinfo

try:
import pypcode
except ImportError:
pypcode = None

import cle

Expand Down Expand Up @@ -32,8 +40,9 @@ def check_sparse_elf(name):
tls = ld.tls.new_thread()

for obj in (extern, tls):
# the main object starts at 0xf800; anything placed inside its span is unreachable
assert obj.max_addr < 0xF800
# the main object's memory is one backer spanning 0xf800 to 0xfff00fff, so anything
# placed inside that span is unreachable
assert obj.max_addr < 0xF800 or obj.min_addr > 0xFFF00FFF
ld.memory.unpack_word(obj.min_addr)


Expand Down Expand Up @@ -69,7 +78,90 @@ def test_rebase_granularity_is_not_a_hard_object_limit():
assert ld.find_object_containing(obj.min_addr) is obj


def load_blob(name, arch, base_addr):
path = os.path.join(TEST_BASE, "tests", *name)
return cle.Loader(path, auto_load_libs=False, main_opts={"backend": "blob", "arch": arch, "base_addr": base_addr})


def test_image_in_the_top_half_leaves_the_null_page_free():
"""
An image based at 0x90000000 has free space above it and below it. The extern object may
go either way, but not over address 0, which every uninitialized pointer in the target
reads as.
"""
ld = load_blob(("armel", "i2c_master_read-nucleol152re.bin"), "ARMEL", 0x90000000)
assert ld.main_object.max_addr >= 2**31

extern = ld.extern_object
assert extern.min_addr > ld.main_object.max_addr
assert ld.find_object_containing(0) is None


@unittest.skipUnless(pypcode is not None, "pypcode not installed")
def test_narrow_address_space_leaves_the_null_page_free():
"""
A 16-bit image at 0x4000 leaves 48 KB above it, and a z80 reaches address 0 with RST and
a direct-page call, so the extern object must not answer for those addresses.
"""
ld = load_blob(("i386", "rcr_test"), archinfo.ArchPcode("z80:LE:16:default"), 0x4000)

extern = ld.extern_object
assert extern.min_addr > ld.main_object.max_addr
assert extern.max_addr < 2**16
assert ld.find_object_containing(0) is None
ld.memory.unpack_word(extern.min_addr, size=1)


@unittest.skipUnless(pypcode is not None, "pypcode not installed")
def test_null_page_is_used_when_the_address_space_has_nothing_else():
"""
Keeping the null page free is a preference, not a constraint. A 16-bit address space with
everything else taken must still place an object rather than fail the load.
"""
ld = load_blob(("i386", "rcr_test"), archinfo.ArchPcode("z80:LE:16:default"), 0x4000)
arch = ld.main_object.arch
main = ld.main_object

above = MockBackend(2**16 - (main.max_addr + 1), arch=arch)
below = MockBackend(main.min_addr - 0x1000, arch=arch)
for obj in (above, below):
ld.dynamic_load(obj)
assert (above.min_addr, above.max_addr) == (main.max_addr + 1, 2**16 - 1)
assert (below.min_addr, below.max_addr) == (0x1000, main.min_addr - 1)

last = MockBackend(0x100, arch=arch)
ld.dynamic_load(last)
assert last.min_addr == 0


def test_null_guard_tracks_the_loader_page_size():
"""
Where the loader is told the target's page size, the guard region at the bottom of the
address space is one such page rather than the conventional 4 KB.
"""
path = os.path.join(TEST_BASE, "tests", "armel", "i2c_master_read-nucleol152re.bin")
ld = cle.Loader(
path,
auto_load_libs=False,
page_size=0x2000,
main_opts={"backend": "blob", "arch": "ARMEL", "base_addr": 0x90000000},
)
main = ld.main_object
arch = main.arch

above = MockBackend(2**32 - (main.max_addr + 1), arch=arch)
below = MockBackend(main.min_addr - 0x2000, arch=arch)
for obj in (above, below):
ld.dynamic_load(obj)
assert (above.min_addr, above.max_addr) == (main.max_addr + 1, 2**32 - 1)
assert (below.min_addr, below.max_addr) == (0x2000, main.min_addr - 1)


if __name__ == "__main__":
test_sparse_main_object()
test_sparse_main_object_unsorted_program_headers()
test_rebase_granularity_is_not_a_hard_object_limit()
test_image_in_the_top_half_leaves_the_null_page_free()
test_narrow_address_space_leaves_the_null_page_free()
test_null_page_is_used_when_the_address_space_has_nothing_else()
test_null_guard_tracks_the_loader_page_size()
Loading