Skip to main content

ThreadX

Protecting a ThreadX image with RunSafe Protect wraps the AArch64 GNU build so the image is randomized when startup reaches main. The board entry point stays the same.

Supported Environment​

This guide applies to:

  • AArch64 Cortex-A55
  • The GNU aarch64-none-elf toolchain
  • Eclipse ThreadX using the Cortex-A55 GNU port

For other architectures, ports, or toolchains, contact [email protected].

Prerequisites​

Before beginning, ensure that:

  • The aarch64-threadx-elf package is provided through the Debian package manager. See Debian/Ubuntu for installation instructions. Protect links that static library into the image.
  • You have a RunSafe license key. See Specify Your License.

Modules​

ThreadX modules are not randomized. Build them with the real aarch64-none-elf toolchain, outside the lfr-helper command in Integrate the Build. A stock module loads and runs on a protected resident image.

Integrate the Build​

Wrap the resident image build with lfr-helper, the ThreadX target, and an entropy source. Rebuild from a clean tree.

make firmware

becomes

# Optional. Modules are not randomized, so build them outside lfr-helper.
make modules
RUNSAFE_LICENSE_KEY=<your license key> \
lfr-helper --target=aarch64-threadx-elf --threadx-rng=imx9-ele \
make firmware

--target=aarch64-threadx-elf applies to the final aarch64-none-elf link of the resident image. Keep the linker script the build already passes with -T. Protect adjusts that script.

Entropy​

Pass one entropy source on the lfr-helper command. Protect does not choose one for you. Without a seed, a ThreadX image stops instead of booting an unrandomized layout.

OptionUse
--threadx-rng=imx9-elei.MX 9 EdgeLock Enclave. Use this only on a board that has that peripheral. On any other memory map those accesses can corrupt memory or hang.
--threadx-rng=cntpctFallback for QEMU or a board with no hardware entropy source. Not recommended for production.

For another entropy source, contact [email protected].

Boot Code and Vectors​

Protect randomizes an image by moving the code in .text. Code whose address must stay fixed belongs outside that section. If it is inside .text, the shuffle moves it and the board enters the wrong place.

Boot code and the exception vector table require fixed locations because the processor enters them at addresses chosen by the board. Put fixed code sections in their own output sections rather than .text. Protect leaves those sections where the link placed them. A port that already collects them as .boot and .vectors needs no change.

Example Linkerscript​

ENTRY(_start)

.boot : { KEEP(*(.boot)) }
.vectors : { KEEP(*(.vectors)) }
.text : { *(.text .text.*) }

Code that can move stays in *(.text) *(.text.*). That is the code Protect shuffles.

The fixed boot path runs first and keeps its address. Randomization runs when that path reaches main, after stacks, BSS, and __top_of_ram are ready. If the first movable entry has another name, pass --traplinker-intercept-symbol as described in Optional Link Flags, or call _TRaP_lfr_preinit from the fixed startup at that same point:

bl      _TRaP_lfr_preinit

lfr-helper accepts --target and --threadx* options. The options below are link options, so they belong on the link command that lfr-helper runs:

RUNSAFE_LICENSE_KEY=<your license key> \
lfr-helper --target=aarch64-threadx-elf --threadx-rng=imx9-ele \
<your link command> -Wl,--traplinker-aarch64-lfr-branch-form=long
FlagWhen to use it
-Wl,--traplinker-aarch64-lfr-branch-form=longThe randomized image does not fit in a short branch (±128 MiB). Long form can clobber x16. Short form is the default and does not.
-Wl,--traplinker-intercept-symbol=<name>Startup calls <name> instead of main.
-Wl,--traplinker-threadx-lfr-heap-size=<bytes>Override the temporary heap size. The default is about twice the randomized code.

Verify the Integration​

See Verify Integration.

Troubleshooting​

Debug output​

Logging is optional. ThreadX does not provide a standardized means of output, so print Protect's log by compiling a function that matches this prototype into console_write.o and linking it into your project:

ssize_t _TRaP_lfr_console_write(const uint8_t *buf, size_t len);

Return the number of bytes written, or a negative errno value. If the function is absent, log output is discarded.

Bake a debug level into the image and boot once:

lfr-helper --target=aarch64-threadx-elf --threadx-rng=cntpct \
--threadx-env=LFR_DEBUG_LEVEL=1 \
make firmware

LFR_DEBUG_LEVEL=1 prints Finished randomizing. --threadx-env may be repeated. Each value is KEY=VALUE. There is no process environment at boot, so these settings are stored in the image. Do not also link your own environ symbol; the link fails with a duplicate definition.

_TRaP_lfr_console_write runs during randomization. If that definition lives in .text, pin the object file that provides it. Here that file is console_write.o:

{
"version": 1,
"blocked": [
{"object": "console_write.o"}
]
}

Pass that file on the link command lfr-helper runs:

lfr-helper --target=aarch64-threadx-elf --threadx-rng=cntpct \
--threadx-env=LFR_DEBUG_LEVEL=1 \
<your link command> -Wl,--traplinker-blocklist=/path/to/blocklist.json

When _TRaP_lfr_console_write is not provided, or its object file is already outside .text, omit the file.

Fixed layout with --threadx-chacha-key​

To boot the same layout every time, pass a key instead of --threadx-rng:

lfr-helper --target=aarch64-threadx-elf \
--threadx-chacha-key='0 1 2 3 4 5 6 7 8 9' \
make firmware

The value is ten whitespace-separated 32-bit hex numbers, without a 0x prefix. The key is stored in the image. When this option is combined with --threadx-rng, the key is the one that is used.

The image stops during startup​

Confirm that the build passed --threadx-rng or --threadx-chacha-key. ThreadX builds do not continue without a seed.

Also confirm that stacks and BSS are ready, and that __top_of_ram holds a non-zero unused RAM address, before main or _TRaP_lfr_preinit. The GNU Cortex-A port does this in its low-level initialization.

Duplicate environ​

The image already defines environ, and lfr-helper was also given --threadx-rng, --threadx-chacha-key, or --threadx-env. Keep one of those sources.

--threadx-rng=imx9-ele hangs or faults​

That option touches the i.MX 9 EdgeLock Enclave at a fixed address. Use it only on a board with that peripheral. Use --threadx-rng=cntpct on QEMU.

The resident image contains a pinned object, and the link did not pass -T. Pass the linker script the board build already uses.

.txtrp is missing​

Confirm that:

  • lfr-helper --target=aarch64-threadx-elf wrapped the compile and the link of the resident image.
  • The tree was cleaned before rebuilding, so objects compiled outside lfr-helper were not reused.
  • The aarch64-threadx-elf package is installed. See Debian/Ubuntu for installation instructions.
  • The link used aarch64-none-elf-gcc through lfr-helper.

If the issue continues, send the full link command and the output of readelf -S on the image to [email protected].