diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eaae1b5..5c294b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -362,4 +362,14 @@ jobs: - name: Test external module with dmod_loader run: | - ${GITHUB_WORKSPACE}/build/examples/system/dmod_loader ${GITHUB_WORKSPACE}/build/dmf/external_test_make.dmf \ No newline at end of file + ${GITHUB_WORKSPACE}/build/examples/system/dmod_loader ${GITHUB_WORKSPACE}/build/dmf/external_test_make.dmf + arm-pointer-relocations: + name: ARM absolute pointer relocation metadata + runs-on: ubuntu-latest + container: + image: chocotechnologies/dmod:1.0.4 + steps: + - uses: actions/checkout@v4 + - run: | + command -v arm-none-eabi-gcc + python3 tests/integration/test_arm_pointer_relocations.py diff --git a/docs/absolute-pointer-relocations.md b/docs/absolute-pointer-relocations.md new file mode 100644 index 0000000..b607d63 --- /dev/null +++ b/docs/absolute-pointer-relocations.md @@ -0,0 +1,29 @@ +# Absolute pointer relocations on ARM and x86-64 + +PIC alone does not relocate pointers embedded in static aggregates. GCC can +also create such aggregates while optimizing a local structure initializer. +Callbacks and third-party constant dispatch tables can therefore contain link +offsets even when every instruction is position independent. + +After `dmod_setup_external_module()`, call `dmod_enable_pointer_relocations()` +before defining modules. The SDK preserves ELF relocation metadata and +records R_ARM_ABS32/R_ARM_TARGET1 or R_X86_64_64 pointer locations in the DMF. It leaves the +existing header, API, GOT and license relocations to their existing handlers. +Non-pointer integers are not guessed or scanned for possible addresses. +Unsupported absolute instruction relocations fail the build. + +The implementation supports ARM ELF32 and x86-64 ELF64 and needs Python 3 at build time. Linker relaxation is disabled so the static link cannot turn GOT references into absolute instruction immediates. Use host tools and a +runtime built from an SDK supporting this feature. An opted-in module has a +256-byte header: the unchanged 240-byte legacy header followed by a 16-byte +extension (magic 0x524c4f43, version 1, table byte offset, record count). The +legacy HeaderSize and footer header-section size describe the extended header. +The table is appended after the original binary image and contains sorted +32-bit byte offsets. Each identifies a pointer-sized module-relative value. +The loader validates the entire table before applying it after the existing +section relocations. Thumb function pointer bits are preserved. Crossplatform +inspection validates the directory without writing native-size pointers. + +Older loaders reject the extended HeaderSize. Legacy modules retain their +original format and still load with the updated runtime. This is opt-in and +currently limited to ARM ELF32 and x86-64 ELF64; other architectures need their own reviewed +relocation mapping before enabling it. diff --git a/inc/dmod_types.h b/inc/dmod_types.h index 7c8f933..e704e55 100644 --- a/inc/dmod_types.h +++ b/inc/dmod_types.h @@ -175,6 +175,18 @@ typedef struct bool ManualLoad; } Dmod_ModuleHeader_t; +/* Optional extension, immediately following the legacy module header. Older + * loaders reject the larger HeaderSize instead of executing unrelocated code. + * Records are sorted uint32_t offsets of pointer-sized module-relative values. */ +#define DMOD_POINTER_RELOCATIONS_MAGIC 0x524C4F43u +#define DMOD_POINTER_RELOCATIONS_VERSION 1u +typedef struct { + uint32_t Magic; + uint32_t Version; + uint32_t Offset; + uint32_t Count; +} Dmod_PointerRelocations_t; + /** * @brief DMFC Header * diff --git a/inc/private/dmod_ldr.h b/inc/private/dmod_ldr.h index f498a0e..44b4a91 100644 --- a/inc/private/dmod_ldr.h +++ b/inc/private/dmod_ldr.h @@ -16,9 +16,11 @@ extern bool Dmod_Ldr_LoadFooter( Dmod_Context_t* Context ); extern bool Dmod_Ldr_LoadOutput( Dmod_Context_t* Context ); extern bool Dmod_Ldr_LoadInput( Dmod_Context_t* Context ); extern bool Dmod_Ldr_LoadGot( Dmod_Context_t* Context ); +extern bool Dmod_Ldr_LoadPointerRelocations(Dmod_Context_t* Context); extern bool Dmod_Ldr_LoadBss( Dmod_Context_t* Context ); #ifdef __cplusplus } #endif + #endif // DMOD_LDR_H \ No newline at end of file diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt index 7866054..2e49fcd 100644 --- a/scripts/CMakeLists.txt +++ b/scripts/CMakeLists.txt @@ -1,3 +1,8 @@ +# Enable SDK-generated relocations for absolute pointers in module constants. +function(dmod_enable_pointer_relocations) + set(DMOD_RELOCATE_ABSOLUTE_POINTERS ON PARENT_SCOPE) +endfunction() + if(NOT DEFINED DMOD_SCRIPTS_DIR) set(DMOD_SCRIPTS_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE STRING "Path to the scripts directory") endif() @@ -768,6 +773,17 @@ function(dmod_create_module moduleName version moduleType) target_link_options(${moduleName} PRIVATE -L ${DMOD_SCRIPTS_DIR} -pie -nostartfiles -nostdlib -Xlinker --discard-all -Wl,--gc-sections,--undefined=ModuleHeader ) target_link_options(${moduleName} PRIVATE -T ${DMOD_SCRIPTS_DIR}/module.ld -static) + # Static initializers can contain absolute pointers outside the GOT, + # including compiler-generated aggregate constants and third-party tables. + # Opt in until all deployed loaders understand the extended header. + if(DMOD_RELOCATE_ABSOLUTE_POINTERS) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + target_compile_definitions(${moduleName} PRIVATE DMOD_RELOCATE_ABSOLUTE_POINTERS=1) + target_link_options(${moduleName} PRIVATE -Wl,--emit-relocs,--no-relax) + set_property(TARGET ${moduleName} APPEND PROPERTY LINK_DEPENDS + ${DMOD_SCRIPTS_DIR}/relocate_pointers.py) + endif() + set(ELF_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${moduleName}") set(DMF_OUTPUT "${DMOD_DMF_DIR}/${moduleName}.dmf") set(DMFC_OUTPUT "${DMOD_DMFC_DIR}/${moduleName}.dmfc") @@ -781,6 +797,13 @@ function(dmod_create_module moduleName version moduleType) COMMENT "Converting ELF to binary" ) + if(DMOD_RELOCATE_ABSOLUTE_POINTERS) + add_custom_command(TARGET ${moduleName} POST_BUILD + COMMAND ${Python3_EXECUTABLE} ${DMOD_SCRIPTS_DIR}/relocate_pointers.py + ${ELF_OUTPUT} ${DMF_OUTPUT} + COMMENT "Recording absolute pointer relocations" VERBATIM) + endif() + if(TODMFC) add_custom_command( TARGET ${moduleName} POST_BUILD diff --git a/scripts/dmod_header.c.in b/scripts/dmod_header.c.in index 0133391..5874e0b 100644 --- a/scripts/dmod_header.c.in +++ b/scripts/dmod_header.c.in @@ -61,4 +61,10 @@ volatile const Dmod_ModuleHeader_t ModuleHeader DMOD_SECTION(".header") DMOD_USE .ManualLoad = @DMOD_MANUAL_LOAD@, }; -volatile const Dmod_ModuleHeader_t* DMOD_Header DMOD_GLOBAL_POINTER = &ModuleHeader; \ No newline at end of file +volatile const Dmod_ModuleHeader_t* DMOD_Header DMOD_GLOBAL_POINTER = &ModuleHeader; +#if defined(DMOD_RELOCATE_ABSOLUTE_POINTERS) && DMOD_RELOCATE_ABSOLUTE_POINTERS +volatile const Dmod_PointerRelocations_t Dmod_PointerRelocations + DMOD_SECTION(".header.relocations") DMOD_USED = { + DMOD_POINTER_RELOCATIONS_MAGIC, DMOD_POINTER_RELOCATIONS_VERSION, 0, 0 + }; +#endif diff --git a/scripts/install-sdk.cmake b/scripts/install-sdk.cmake index c158e12..94cc710 100644 --- a/scripts/install-sdk.cmake +++ b/scripts/install-sdk.cmake @@ -31,7 +31,7 @@ install(DIRECTORY "${DMOD_DIR}/lib/third-party/FastLZ" PATTERN "LICENSE.MIT") install(DIRECTORY "${DMOD_DIR}/scripts/" DESTINATION "${DMOD_SDK_INSTALL_DIR}/scripts" COMPONENT development - FILES_MATCHING PATTERN "CMakeLists.txt" PATTERN "*.in" PATTERN "*.ld" + FILES_MATCHING PATTERN "CMakeLists.txt" PATTERN "*.in" PATTERN "*.ld" PATTERN "*.py" PATTERN "memory_analysis.cmake") install(DIRECTORY "${DMOD_DIR}/cmake/runtime" DESTINATION "${DMOD_SDK_INSTALL_DIR}/cmake" COMPONENT development) diff --git a/scripts/module.ld b/scripts/module.ld index cc09fd3..d8ddded 100644 --- a/scripts/module.ld +++ b/scripts/module.ld @@ -17,6 +17,7 @@ SECTIONS { __header_start = .; *(.header) + KEEP(*(.header.relocations)) __header_end = .; } : header diff --git a/scripts/relocate_pointers.py b/scripts/relocate_pointers.py new file mode 100644 index 0000000..36be467 --- /dev/null +++ b/scripts/relocate_pointers.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Add explicit absolute pointer fixups to an ARM ELF32 or x86-64 DMF (stdlib only). + +--emit-relocs preserves linker-resolved references. PC-relative references need +no fixup. Existing header/API/GOT/license relocations remain owned by the loader. +Unknown absolute instruction relocations are rejected instead of silently +producing a non-position-independent module. +""" +import struct +import sys +from pathlib import Path + + +def convert(elf, binary): + if len(elf) < 64 or elf[:4] != b'\x7fELF' or elf[5] != 1 or elf[4] not in (1,2): + raise ValueError('Expected little-endian ELF32/ELF64') + machine = struct.unpack_from('= count: + raise ValueError('Invalid section directory') + sections = [struct.unpack_from(section_format,elf,shoff+i*entsize) for i in range(count)] + names = sections[names_index] + strings = elf[names[4]:names[4] + names[5]] + def name(section): + start = section[0] + return strings[start:strings.index(b'\0', start)].decode('ascii') + by_name = {name(s): s for s in sections} + header = by_name['.header'] + if header[3] != 0 or header[5] != 256: + raise ValueError('Expected legacy 240-byte header plus 16-byte relocation extension') + if struct.unpack_from('= width: + excluded.append((value + size - width, value + size)) + offsets = set() + for section in sections: + if section[1] != (4 if wide else 9): # SHT_RELA / SHT_REL + continue + target = sections[section[7]] + if not target[2] & 2: + continue + symbols = sections[section[6]] + record_size = 24 if wide else 8 + if section[9] != record_size: + raise ValueError('Invalid relocation record size') + for pos in range(section[4],section[4]+section[5],record_size): + offset,info = struct.unpack_from('>32) if wide else (info&255,info>>8) + if any(start <= offset < end for start,end in excluded): + continue + symbol_index = struct.unpack_from(' len(binary): + raise ValueError('Pointer relocation outside image') + value = struct.unpack_from('= len(binary): + raise ValueError('Absolute pointer outside module') + offsets.add(offset) + elif kind not in relative: + raise ValueError(f'Unsupported relocation {kind} at {offset:#x}') + result = bytearray(binary) + while len(result) % 4: + result.append(0) + table = len(result) + for offset in sorted(offsets): + result.extend(struct.pack('HeaderSize != sizeof( Dmod_ModuleHeader_t ) ) + if( header->HeaderSize != sizeof( Dmod_ModuleHeader_t ) + && header->HeaderSize != sizeof(Dmod_ModuleHeader_t) + sizeof(Dmod_PointerRelocations_t) ) { DMOD_LOG_ERROR("Cannot load header - invalid header size: %d != %d\n", header->HeaderSize, sizeof( Dmod_ModuleHeader_t ) ); return false; @@ -151,7 +152,7 @@ bool Dmod_Ldr_LoadFooter( Dmod_Context_t* Context ) Dmod_ModuleFooter_t* footer = Context->Header->Footer.Ptr; - if( footer->Header.SectionStart != 0 || footer->Header.SectionSize != sizeof( Dmod_ModuleHeader_t ) ) + if( footer->Header.SectionStart != 0 || footer->Header.SectionSize != Context->Header->HeaderSize ) { DMOD_LOG_ERROR("Cannot load footer - invalid header section\n"); return false; @@ -593,6 +594,50 @@ bool Dmod_Ldr_LoadBss( Dmod_Context_t* Context ) return true; } +/* Validate the whole table before applying any fixup. Do not trust counts, + * offsets or pointer alignment from a downloaded module. */ +bool Dmod_Ldr_LoadPointerRelocations(Dmod_Context_t* Context) +{ + if(Context == NULL || Context->Header == NULL) return false; + if(Context->Header->HeaderSize == sizeof(Dmod_ModuleHeader_t)) return true; + if(Context->Header->HeaderSize != sizeof(Dmod_ModuleHeader_t) + sizeof(Dmod_PointerRelocations_t) + || Context->Size < Context->Header->HeaderSize) return false; + Dmod_PointerRelocations_t extension; + memcpy(&extension, (uint8_t*)Context->Data + sizeof(Dmod_ModuleHeader_t), sizeof(extension)); + if(extension.Magic != DMOD_POINTER_RELOCATIONS_MAGIC + || extension.Version != DMOD_POINTER_RELOCATIONS_VERSION + || extension.Offset < Context->Header->HeaderSize + || extension.Offset > Context->Size + || extension.Count > (Context->Size - extension.Offset) / sizeof(uint32_t)) return false; + /* Both native loading and crossplatform inspection validate every record. + * Only native loading applies the already validated pointer fixups. */ + size_t width = Context->Header->PointerSize; + if(width != 4 && width != 8) return false; + if(!Dmod_SystemCrossplatformMode && width != sizeof(uintptr_t)) return false; + uint32_t previous = 0; + for(size_t i = 0; i < extension.Count; ++i) { + uint32_t offset; + memcpy(&offset, (uint8_t*)Context->Data + extension.Offset + i * sizeof(offset), sizeof(offset)); + if(offset < Context->Header->HeaderSize || offset > extension.Offset + || width > extension.Offset - offset || (i && (offset <= previous || offset - previous < width))) return false; + uint64_t value = 0; + memcpy(&value, (uint8_t*)Context->Data + offset, width); + if(value >= extension.Offset) return false; + if(!Dmod_SystemCrossplatformMode && value > UINTPTR_MAX - (uintptr_t)Context->Data) return false; + previous = offset; + } + if(!Dmod_SystemCrossplatformMode) { + for(size_t i = 0; i < extension.Count; ++i) { + uint32_t offset; uintptr_t value; + memcpy(&offset, (uint8_t*)Context->Data + extension.Offset + i * sizeof(offset), sizeof(offset)); + memcpy(&value, (uint8_t*)Context->Data + offset, width); + value += (uintptr_t)Context->Data; + memcpy((uint8_t*)Context->Data + offset, &value, width); + } + } + return true; +} + /** * @brief Load module * @@ -612,7 +657,8 @@ bool Dmod_Ldr_Load( Dmod_Context_t* Context ) || !Dmod_Ldr_LoadOutput( Context ) || !Dmod_Ldr_LoadInput( Context ) || !Dmod_Ldr_LoadGot( Context ) - || !Dmod_Ldr_LoadBss( Context ) ) + || !Dmod_Ldr_LoadBss( Context ) + || !Dmod_Ldr_LoadPointerRelocations( Context ) ) { return false; } diff --git a/tests/integration/test_arm_pointer_relocations.py b/tests/integration/test_arm_pointer_relocations.py new file mode 100644 index 0000000..1a0c93e --- /dev/null +++ b/tests/integration/test_arm_pointer_relocations.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Compile real ARM aggregates and verify the DMF relocation metadata.""" +import importlib.util +from pathlib import Path +import shutil +import struct +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] +spec = importlib.util.spec_from_file_location('relocate', ROOT / 'scripts/relocate_pointers.py') +relocate = importlib.util.module_from_spec(spec) +spec.loader.exec_module(relocate) + +class PointerRelocations(unittest.TestCase): + @unittest.skipUnless(shutil.which('arm-none-eabi-gcc'), 'ARM GNU toolchain required') + def test_arm_aggregates(self): + self.aggregate_fixture('arm-none-eabi-', ['-mcpu=cortex-m7','-mthumb'],4) + + def test_x86_64_aggregates(self): + self.aggregate_fixture('',[],8) + + def aggregate_fixture(self,prefix,flags,width): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root/'input.c').write_text(''' +#include +__attribute__((section(".header"),used)) const uint32_t header[60] = {0x444d4f44,240,256,4}; +__attribute__((section(".header.relocations"),used)) const uint32_t extension[4] = {0x524c4f43,1,0,0}; +struct dispatch {void (*fn)(void);const char* text;unsigned number;}; +static void callback(void) {__asm__ volatile ("nop");} +static const char text[]="pointer test"; +const struct dispatch dispatch = {callback,text,0x151}; +__attribute__((noinline)) void consume(const struct dispatch* d) {__asm__ volatile (""::"r"(d):"memory");} +void entry(void) {struct dispatch local={callback,text,0x151};consume(&local);consume(&dispatch);} +'''.replace('240,256,4','240,256,'+str(width))) + (root/'layout.ld').write_text(''' +SECTIONS { + . = 0; .header : {KEEP(*(.header)) KEEP(*(.header.relocations))} + .text : {*(.text*)} .rodata : {*(.rodata*)} .data : {*(.data*)} + .got : {*(.got*)} .bss : {*(.bss*)} + /DISCARD/ : {*(.ARM.exidx*) *(.ARM.extab*) *(.comment*) *(.note*)} +} +''') + subprocess.run([prefix+'gcc',*flags,'-fPIC','-fPIE', + '-O2','-nostdlib','-static','-Wl,--emit-relocs,--no-relax','-T',str(root/'layout.ld'), + str(root/'input.c'),'-o',str(root/'input.elf')],check=True,capture_output=True) + subprocess.run([prefix+'objcopy','-O','binary',str(root/'input.elf'),str(root/'input.dmf')],check=True) + binary=(root/'input.dmf').read_bytes() + result,count=relocate.convert((root/'input.elf').read_bytes(),binary) + self.assertGreaterEqual(count,4 if width==4 else 2) # static table and compiler aggregate + offset,records=struct.unpack_from('(image); + header->HeaderSize = sizeof(*header) + sizeof(Dmod_PointerRelocations_t); + header->PointerSize = sizeof(uintptr_t); + Dmod_PointerRelocations_t ext = {DMOD_POINTER_RELOCATIONS_MAGIC, 1, 480, 2}; + memcpy(image + sizeof(*header), &ext, sizeof(ext)); + uintptr_t data = 400, callback = 401, integer = 17; + memcpy(image + 352, &data, sizeof(data)); + memcpy(image + 368, &callback, sizeof(callback)); + memcpy(image + 384, &integer, sizeof(integer)); + uint32_t offsets[] = {352, 368}; + memcpy(image + 480, offsets, sizeof(offsets)); + Dmod_Context_t context = {}; + context.Data = image; context.Size = sizeof(image); context.Header = header; + Dmod_SystemCrossplatformMode = false; + ASSERT_TRUE(Dmod_Ldr_LoadPointerRelocations(&context)); + memcpy(&data, image + 352, sizeof(data)); + memcpy(&callback, image + 368, sizeof(callback)); + memcpy(&integer, image + 384, sizeof(integer)); + EXPECT_EQ(data, reinterpret_cast(image) + 400); + EXPECT_EQ(callback, reinterpret_cast(image) + 401); + EXPECT_EQ(integer, 17u); +} + +TEST_F(DmodLdrTest, AbsolutePointersRejectMalformedTablesBeforeAnyMutation) +{ + alignas(uintptr_t) unsigned char image[512] = {}; + auto* header = reinterpret_cast(image); + header->HeaderSize = sizeof(*header) + sizeof(Dmod_PointerRelocations_t); + header->PointerSize = sizeof(uintptr_t); + Dmod_Context_t context = {}; + context.Data = image; context.Size = sizeof(image); context.Header = header; + Dmod_SystemCrossplatformMode = false; + struct Invalid { uint32_t magic, version, offset, count, first, second; } cases[] = { + {0, 1, 480, 1, 352, 0}, + {DMOD_POINTER_RELOCATIONS_MAGIC, 2, 480, 1, 352, 0}, + {DMOD_POINTER_RELOCATIONS_MAGIC, 1, 513, 0, 352, 0}, + {DMOD_POINTER_RELOCATIONS_MAGIC, 1, 480, UINT32_MAX, 352, 0}, + {DMOD_POINTER_RELOCATIONS_MAGIC, 1, 480, 2, 352, 352}, + {DMOD_POINTER_RELOCATIONS_MAGIC, 1, 480, 2, 352, 353}, + {DMOD_POINTER_RELOCATIONS_MAGIC, 1, 480, 2, 352, 479}, + {DMOD_POINTER_RELOCATIONS_MAGIC, 1, 480, 1, 8, 0}, + }; + for(auto test : cases) { + Dmod_PointerRelocations_t ext = {test.magic, test.version, test.offset, test.count}; + memcpy(image + sizeof(*header), &ext, sizeof(ext)); + uintptr_t value = 400; + memcpy(image + 352, &value, sizeof(value)); + uint32_t offsets[] = {test.first, test.second}; + memcpy(image + 480, offsets, sizeof(offsets)); + EXPECT_FALSE(Dmod_Ldr_LoadPointerRelocations(&context)); + uintptr_t after; + memcpy(&after, image + 352, sizeof(after)); + EXPECT_EQ(after, value); + } +} + +TEST_F(DmodLdrTest, AbsolutePointersAllowLegacyImages) +{ + Dmod_ModuleHeader_t header = {}; + header.HeaderSize = sizeof(header); + Dmod_Context_t context = {}; + context.Header = &header; + EXPECT_TRUE(Dmod_Ldr_LoadPointerRelocations(&context)); + EXPECT_FALSE(Dmod_Ldr_LoadPointerRelocations(nullptr)); +} + +TEST_F(DmodLdrTest, CrossplatformInspectionValidatesEveryRecordWithoutRelocating) +{ + unsigned char image[512] = {}; + auto* header = reinterpret_cast(image); + header->HeaderSize = sizeof(*header) + sizeof(Dmod_PointerRelocations_t); + header->PointerSize = 4; + Dmod_PointerRelocations_t ext = {DMOD_POINTER_RELOCATIONS_MAGIC, 1, 480, 1}; + memcpy(image + sizeof(*header), &ext, sizeof(ext)); + uint32_t offset=352, value=400; + memcpy(image+480,&offset,sizeof(offset)); + memcpy(image+352,&value,sizeof(value)); + Dmod_Context_t context = {}; + context.Data=image;context.Size=sizeof(image);context.Header=header; + Dmod_SystemCrossplatformMode=true; + EXPECT_TRUE(Dmod_Ldr_LoadPointerRelocations(&context)); + uint32_t after=0;memcpy(&after,image+352,sizeof(after)); + EXPECT_EQ(after,value); + offset=511;memcpy(image+480,&offset,sizeof(offset)); + EXPECT_FALSE(Dmod_Ldr_LoadPointerRelocations(&context)); + Dmod_SystemCrossplatformMode=false; +}