Skip to content

free_space.py

ofrak.core.free_space

Allocatable (ResourceView) dataclass

Identifies a resource that may have some free space available within it to allocate (for arbitrary purposes). Once tagged as an Allocatable, a resource may be analyzed to find all of the available free space within it and its descendants. These ranges are stored as minimal lists of contiguous ranges of free space (ranges which overlap or touch are combined), sorted first by size then vaddr (lowest to highest). Each list of free space ranges contains only free space with one type of memory access permissions of the ranges, i.e. all read-only free space is stored in one list, all read-execute free-space is stored in another list, etc.

allocate(self, permissions, requested_size, alignment=4, min_fragment_size=None, within_range=None) async

Request some range(s) of free space which satisfies the given constraints as parameters. If such ranges are found, the resources they belong to (which are descendants of this Allocatable) are modified to reflect that part or all of them are no longer valid free space using RemoveFreeSpaceModifier.

Allows either a fragmented allocation (X space is allocated across N individual ranges), or a non-fragmented allocation (one range of size X). To force the allocation to be non-fragmented, set the min_fragment_size param equal to the requested_size.

Parameters:

Name Type Description Default
permissions MemoryPermissions

Required memory permissions of the free space (exact match)

required
requested_size int

Total size of allocated ranges

required
alignment int

That start of the allocated ranges will be aligned to alignment bytes

4
min_fragment_size Optional[int]

The minimum size of each allocated range

None
within_range Optional[ofrak_type.range.Range]

All returned ranges must be within this virtual address range

None

Returns:

Type Description
List[ofrak_type.range.Range]

A list of one or more Range, which each contain a start and end vaddr of an allocated range of free space

Exceptions:

Type Description
FreeSpaceAllocationError

if there is not enough free space to allocate which matches the given constraints

Source code in ofrak/core/free_space.py
async def allocate(
    self,
    permissions: MemoryPermissions,
    requested_size: int,
    alignment: int = 4,
    min_fragment_size: Optional[int] = None,
    within_range: Optional[Range] = None,
) -> List[Range]:
    """
    Request some range(s) of free space which satisfies the given constraints as parameters.
    If such ranges are found, the resources they belong to (which are descendants of this
    `Allocatable`) are modified to reflect that part or all of them are no longer valid free
    space using
    [RemoveFreeSpaceModifier][ofrak.core.free_space.RemoveFreeSpaceModifier].

    Allows either a fragmented allocation (X space is allocated across N individual ranges),
    or a non-fragmented allocation (one range of size X). To force the allocation to be
    non-fragmented, set the `min_fragment_size` param equal to the `requested_size`.

    :param permissions: Required memory permissions of the free space (exact match)
    :param requested_size: Total size of allocated ranges
    :param alignment: That start of the allocated ranges will be aligned to `alignment` bytes
    :param min_fragment_size: The minimum size of each allocated range
    :param within_range: All returned ranges must be within this virtual address range

    :return: A list of one or more [Range][ofrak_type.range.Range], which each contain a
     start and end vaddr of an allocated range of free space

    :raises FreeSpaceAllocationError: if there is not enough free space to allocate which
    matches the given constraints
    """
    allocated_ranges = await self._allocate(
        permissions,
        requested_size,
        alignment,
        min_fragment_size,
        within_range,
    )

    # Having acquired a satisfactory allocation, make sure subsequent calls won't allocate
    # from this same block
    await self.resource.run(
        RemoveFreeSpaceModifier,
        FreeSpaceAllocation(
            permissions,
            allocated_ranges,
        ),
    )
    self.remove_allocation_from_cached_free_ranges(allocated_ranges, permissions)

    return allocated_ranges

allocate_bom(self, bom, permission_map=None) async

Responsible for allocating the patches if free memory is required and providing details about where space was made.

Parameters:

Name Type Description Default
bom BOM required
permission_map Optional[Mapping[ofrak_type.memory_permissions.MemoryPermissions, Iterable[ofrak_type.memory_permissions.MemoryPermissions]]]

a map that assigns patch segment-local permissions to possible destination permissions. available memory pool is evaluated in order! Ex: a developer wants to enable allocation of RO strings from .rodata in an RX .text section.

None

Returns:

Type Description
PatchRegionConfig

information required to generate the linker directive script

Source code in ofrak/core/free_space.py
async def allocate_bom(
    self,
    bom: BOM,
    permission_map: Optional[Mapping[MemoryPermissions, Iterable[MemoryPermissions]]] = None,
) -> PatchRegionConfig:
    """
    Responsible for allocating the patches if free memory is required and
    providing details about where space was made.

    :param bom:
    :param permission_map: a map that assigns patch segment-local permissions to possible
    destination permissions. available memory pool is evaluated in order!
    Ex: a developer wants to enable allocation of RO strings from .rodata in an RX .text section.

    :return: information required to generate the linker directive script
    """
    segments_to_allocate: List[Tuple[AssembledObject, Segment]] = []
    for obj in bom.object_map.values():
        for segment in obj.segment_map.values():
            segments_to_allocate.append((obj, segment))

    # Allocate largest segments first
    segments_to_allocate.sort(key=lambda o_s: o_s[1].length, reverse=True)
    segments_by_object: Dict[str, List[Segment]] = defaultdict(list)
    for obj, segment in segments_to_allocate:
        vaddr, final_size = 0, 0
        if segment.length == 0:
            continue
        if permission_map is not None:
            possible_perms = permission_map[segment.access_perms]
        else:
            possible_perms = (segment.access_perms,)
        for candidate_permissions in possible_perms:
            try:
                allocs = await self.allocate(
                    candidate_permissions,
                    segment.length,
                    min_fragment_size=segment.length,
                    alignment=bom.segment_alignment,
                )
                allocation = next(iter(allocs))
                vaddr = allocation.start
                final_size = allocation.length()
                break
            except FreeSpaceAllocationError:
                continue
        if vaddr == 0 or final_size == 0:
            raise FreeSpaceAllocationError(
                f"Could not find enough free space for access perms {possible_perms} and "
                f"length {segment.length}"
            )
        segments_by_object[obj.path].append(
            Segment(
                segment_name=segment.segment_name,
                vm_address=vaddr,
                offset=segment.offset,
                is_entry=segment.is_entry,
                length=final_size,
                access_perms=segment.access_perms,
            )
        )

    all_segments: Dict[str, Tuple[Segment, ...]] = {
        object_path: tuple(segments) for object_path, segments in segments_by_object.items()
    }

    return PatchRegionConfig(bom.name + "_patch", immutabledict(all_segments))

FreeSpace (MemoryRegion) dataclass

FreeSpace(virtual_address: int, size: int, permissions: ofrak_type.memory_permissions.MemoryPermissions)

FreeSpaceAllocation (ComponentConfig) dataclass

FreeSpaceAllocation(permissions: ofrak_type.memory_permissions.MemoryPermissions, allocations: List[ofrak_type.range.Range])

FreeSpaceAnalyzer (Analyzer)

Analyze an Allocatable resource to find the ranges of free space it contains by searching for descendants tagged as FreeSpace. The ranges of each individual FreeSpace resource will be globbed into as few non-overlapping ranges as possible. The ranges of different types of free space - such as RW permissions vs RX permissions - will be calculated and stored separately.

analyze(self, resource, config=None) async

Analyze a resource for to extract specific ResourceAttributes.

Users should not call this method directly; rather, they should run Resource.run or Resource.analyze.

Parameters:

Name Type Description Default
resource Resource

The resource that is being analyzed

required
config ComponentConfig

Optional config for analyzing. If an implementation provides a default, this default will always be used when config would otherwise be None. Note that a copy of the default config will be passed, so the default config values cannot be modified persistently by a component run.

None

Returns:

Type Description
Allocatable

The analysis results

Source code in ofrak/core/free_space.py
async def analyze(self, resource: Resource, config: ComponentConfig = None) -> Allocatable:
    ranges_by_permissions = defaultdict(list)
    for free_space_r in await resource.get_descendants_as_view(
        FreeSpace,
        r_filter=ResourceFilter.with_tags(FreeSpace),
        r_sort=ResourceSort(FreeSpace.VirtualAddress),
    ):
        ranges_by_permissions[free_space_r.permissions].append(free_space_r.vaddr_range())

    merged_ranges_by_permissions = dict()
    for perms, ranges in ranges_by_permissions.items():
        merged_ranges_by_permissions[perms] = Allocatable.sort_free_ranges(
            Range.merge_ranges(ranges)
        )

    return Allocatable(merged_ranges_by_permissions)

FreeSpaceModifier (Modifier)

Turn a MemoryRegion resource into allocatable free space by replacing its data with b'' or optionally specified bytes.

The modifier allows for an optional "stub", bytes to be injected at the beginning of the target resource. The stub bytes are not marked as FreeSpace.

modify(self, resource, config) async

Modify the given resource.

Users should not call this method directly; rather, they should run Resource.run.

Parameters:

Name Type Description Default
resource Resource required
config FreeSpaceModifierConfig

Optional config for modification. If an implementation provides a default, this default will always be used when config would otherwise be None. Note that a copy of the default config will be passed, so the default config values cannot be modified persistently by a component run.

required
Source code in ofrak/core/free_space.py
async def modify(self, resource: Resource, config: FreeSpaceModifierConfig):
    mem_region_view = await resource.view_as(MemoryRegion)

    freed_range = Range(
        mem_region_view.virtual_address,
        mem_region_view.virtual_address + mem_region_view.size,
    )
    patch_data = _get_patch(freed_range, config.stub, config.fill)
    parent = await resource.get_parent()
    patch_offset = (await resource.get_data_range_within_parent()).start
    patch_range = freed_range.translate(patch_offset - freed_range.start)

    # Grab tags, so they can be saved to the stub.
    # At some point, it might be nice to save the attributes as well.
    current_tags = resource.get_tags()
    # One interesting side effect here is the Resource used to call this modifier no longer exists
    # when this modifier returns. This can be confusing. Would an update work better in this case?
    await resource.delete()
    await resource.save()

    # Patch in the patch_data
    await parent.run(BinaryPatchModifier, BinaryPatchConfig(patch_offset, patch_data))

    free_offset = len(config.stub)

    if len(config.stub) > 0:
        # Create the stub
        await parent.create_child_from_view(
            MemoryRegion(mem_region_view.virtual_address, len(config.stub)),
            data_range=Range.from_size(patch_range.start, len(config.stub)),
            additional_tags=current_tags,
        )

    # Create the FreeSpace child
    await parent.create_child_from_view(
        FreeSpace(
            mem_region_view.virtual_address + free_offset,
            mem_region_view.size - free_offset,
            config.permissions,
        ),
        data_range=Range(patch_range.start + free_offset, patch_range.end),
    )

FreeSpaceModifierConfig (ComponentConfig) dataclass

Configuration for modifier which marks some free space.

Attributes:

Name Type Description
permissions MemoryPermissions

Memory permissions to give the created free space.

stub bytes

Bytes for a stub to be injected before the free space. The stub will not be marked as FreeSpace.

fill bytes

Pattern of bytes to fill the free space with.

PartialFreeSpaceModifier (Modifier)

Turn part of a MemoryRegion resource into allocatable free space by replacing a range of its data with fill bytes (b'' by default).

The modifier supports optionally injecting a "stub", bytes at the beginning of the targeted range that will not be marked as FreeSpace.

modify(self, resource, config) async

Modify the given resource.

Users should not call this method directly; rather, they should run Resource.run.

Parameters:

Name Type Description Default
resource Resource required
config PartialFreeSpaceModifierConfig

Optional config for modification. If an implementation provides a default, this default will always be used when config would otherwise be None. Note that a copy of the default config will be passed, so the default config values cannot be modified persistently by a component run.

required
Source code in ofrak/core/free_space.py
async def modify(self, resource: Resource, config: PartialFreeSpaceModifierConfig):
    freed_range = config.range_to_remove
    mem_region_view = await resource.view_as(MemoryRegion)
    if not freed_range.within(mem_region_view.vaddr_range()):
        raise ModifierError(
            f"Free space range, {freed_range}, must lie within target memory"
            f"region range, {mem_region_view.vaddr_range()}"
        )

    await _find_and_delete_overlapping_children(resource, freed_range)

    patch_offset = mem_region_view.get_offset_in_self(freed_range.start)
    patch_range = Range.from_size(patch_offset, freed_range.length())
    patch_data = _get_patch(freed_range, config.stub, config.fill)
    await mem_region_view.resource.run(
        BinaryPatchModifier, BinaryPatchConfig(patch_offset, patch_data)
    )

    free_offset = len(config.stub)
    await mem_region_view.resource.create_child_from_view(
        FreeSpace(
            freed_range.start + free_offset,
            freed_range.length() - free_offset,
            config.permissions,
        ),
        data_range=Range(patch_range.start + free_offset, patch_range.end),
    )

PartialFreeSpaceModifierConfig (ComponentConfig) dataclass

Attributes:

Name Type Description
permissions MemoryPermissions

memory permissions to give the created free space.

range_to_remove Range

The ranges to consider as free space (remove).

stub bytes

Bytes for a stub to be injected before the free space. If a stub is specified, then the FreeSpace created will decrease in size. For example, with a stub of b"HA" and range_to_remove=Range(4,10), the final FreeSpace will end up corresponding to Range(6,10).

fill bytes

Pattern of bytes to fill the free space with.

RemoveFreeSpaceModifier (Modifier)

After allocating some space from an Allocatable, fix up its descendants to make sure the allocated space will not be allocated again. Remove FreeSpace tags from resources which overlap with an allocated range. If part of one of these resources is not within an allocated range, create a child tagged as FreeSpace to reflect that part of it is still available as free space.

modify(self, resource, config) async

Modify the given resource.

Users should not call this method directly; rather, they should run Resource.run.

Parameters:

Name Type Description Default
resource Resource required
config FreeSpaceAllocation

Optional config for modification. If an implementation provides a default, this default will always be used when config would otherwise be None. Note that a copy of the default config will be passed, so the default config values cannot be modified persistently by a component run.

required
Source code in ofrak/core/free_space.py
async def modify(self, resource: Resource, config: FreeSpaceAllocation) -> None:
    wholly_allocated_resources = list()
    partially_allocated_resources: Dict[bytes, Tuple[FreeSpace, List[Range]]] = dict()
    allocatable = await resource.view_as(Allocatable)

    for alloc in config.allocations:
        for res_wholly_in_alloc in await resource.get_descendants_as_view(
            FreeSpace,
            r_filter=ResourceFilter(
                tags=(FreeSpace,),
                attribute_filters=(
                    ResourceAttributeValueFilter(
                        FreeSpace.Permissions, config.permissions.value
                    ),
                    ResourceAttributeRangeFilter(
                        FreeSpace.VirtualAddress,
                        min=alloc.start,
                        max=alloc.end - 1,
                    ),
                    ResourceAttributeRangeFilter(
                        FreeSpace.EndVaddr, min=alloc.start + 1, max=alloc.end
                    ),
                ),
            ),
        ):
            wholly_allocated_resources.append(res_wholly_in_alloc)

        for res_partially_in_alloc in await self._get_partially_overlapping_resources(
            resource,
            config.permissions,
            alloc,
        ):
            free_space_range = res_partially_in_alloc.vaddr_range()
            overlap = alloc.intersect(free_space_range)
            assert overlap.length() > 0
            free_space_res_id = res_partially_in_alloc.resource.get_id()
            if free_space_res_id in partially_allocated_resources:
                _, allocated_ranges_of_res = partially_allocated_resources[free_space_res_id]
                allocated_ranges_of_res.append(overlap)
            else:
                partially_allocated_resources[free_space_res_id] = (
                    res_partially_in_alloc,
                    [overlap],
                )

    for fs in wholly_allocated_resources:
        fs.resource.remove_tag(FreeSpace)

    for fs, allocated_ranges in partially_allocated_resources.values():
        remaining_free_space_ranges = remove_subranges([fs.vaddr_range()], allocated_ranges)
        for remaining_range in remaining_free_space_ranges:
            remaining_data_range = Range.from_size(
                fs.get_offset_in_self(remaining_range.start), remaining_range.length()
            )
            await fs.resource.create_child_from_view(
                FreeSpace(
                    remaining_range.start,
                    remaining_range.length(),
                    fs.permissions,
                ),
                data_range=remaining_data_range,
            )
        fs.resource.remove_tag(FreeSpace)

    # Update Allocatable attributes, reflecting removed ranges
    allocatable.remove_allocation_from_cached_free_ranges(
        config.allocations, config.permissions
    )
    resource.add_view(allocatable)