Porting the Solana eBPF JIT compiler to ARM64

By Andrew Haberland

Throughout my summer season internship at Path of Bits. I labored on the fork of the RBPF JIT compiler that’s use to execute Solana sensible contracts. The RBPF JIT compiler performs a crucial position on the Solana blockchain. Because it facilitates the execution of contracts on validator nodes by default.

Earlier than my work on this venture, RBPF supported JIT mode solely on x86 hosts. An growing variety of builders are utilizing ARM64 machines, however are unable to run their take a look at in JIT mode. My main purpose was so as to add help in RBPF for the ARM64 architecture. Primarily by updating the register map, calling conference, and all the subroutines and instruction translations to emit ARM64 directions. I additionally aimed to implement help for Home windows within the RBPF x86 JIT compiler.

The work is stay and might discovered in two pull requests on Solana’s GitHub web page. Nevertheless, a caveat: it’s at present behind a feature-gate ('jit-aarch64-not-safe-for-production') and isn’t prepared for manufacturing till it has obtained a radical peer evaluate.

JIT compiler: Background

Sensible contracts that run on the Solana blockchain are compiled from. Rust (or C, should you like bugs) to eBPF, an prolonged model of the Berkeley Packet Filter. The eBPF digital machine’s structure is pretty easy, with a minimal set of 32- and 64-bit integer operations (together with multiplication and division) and reminiscence and management stream directions. BPF applications have their very own tackle house, which in. RBPF consists of code, stack, heap, and enter knowledge sections situated at fastened addresses.

The model of BPF supported by RBPF was design to work with applications compiled utilizing the LLVM BPF again finish. The official Linux documentation for eBPF reveals that there are a couple of variations between RBPF and eBPF—most notably, RBPF has to help an oblique name (callx) instruction.

Moreover, RBPF’s “verifier” is way easier than that of eBPF. Within the Linux kernel, the eBPF verifier validates certain safety properties of BPF applications earlier than JITing and executing them. In RBPF, Solana applications cross via a a lot easier verifier earlier than being JITed. The verifier checks for directions that attempt to divide by a relentless zero Leap to a clearly invalid tackle, or learn or write to an invalid register, amongst different errors. Notably, the RBPF verifier doesn’t carry out any. CFG evaluation or try to trace the vary of values held by every register. The total checklist of errors that the RBPF verifier checks for can discovered here.

RBPF internals

on chain 1

The supply code to binary translation levels

RBPF verifies then interprets a whole program, instruction by instruction, into the goal structure earlier than lastly calling into the emitted code. This includes an eBPF instruction decoder and a partial instruction encoder for the goal structure (earlier than the summer season of 2022, solely x86 was help). RBPF additionally gives an interpreter able to executing eBPF Solana applications, however the JITed translations are the default for efficiency causes.

Reminiscence and tackle translation

BPF applications are execute in their very own reminiscence house, and there’s a mapping between this tackle house and the host tackle house. Reminiscence areas are arrange (utilizing mmap and mprotect) for every program that’s to be executed; the BPF code, stack, heap, and enter knowledge have their very own areas, situated at fixed addresses in BPF tackle house. The areas of those mappings within the host tackle house should not repair.

JIT compiler

The reminiscence format of the vm surroundings

To deal with eBPF load and retailer directions, the tackle should first be translate into the host tackle house. RBPF features a translate_memory_address meeting routine, which is chargeable for wanting up the area that accommodates the tackle being accessed and for translating the BPF tackle into a number tackle. This translation logic is invoke each time a BPF load or retailer instruction is execute, as proven within the instance instruction translations later on this put up.

Register allocation

BPF has 11 registers (10 normal goal registers and the body pointer), every of which maps to a distinct register within the host structure. On x86_64, which has 16 registers, 4 of the remaining registers are use for particular functions (RSP can’t repurpose, because the authentic host name stack will maintained), describe beneath:

// Particular registers:
//     ARGUMENT_REGISTERS[0]  RDI  BPF program counter restrict (utilized by instruction meter)
// CALLER_SAVED_REGISTERS[8]  R11  Scratch register
// CALLER_SAVED_REGISTERS[7]  R10  Fixed pointer to JitProgramArgument (additionally scratch register for exception dealing with)
// CALLEE_SAVED_REGISTERS[0]  RBP  Fixed pointer to preliminary RSP - 8

Supply: Line 224 of jit.rs in solana-labs

Instruction translation

Translating directions in RBPF is a reasonably easy course of:

  • Registers within the eBPF digital machine are map to a singular register within the host structure.
  • Every opcode is translate to a number of directions within the host structure (by way of this massive match assertion).

Two instance translations are show beneath:

Instance instruction translations

RBPF consists of subroutines which can be emitted as soon as to deal with share logic (resembling tackle translation, which is carried out by translating the load instruction above). Typically these subroutines embody calls again into Rust code to deal with extra difficult operations (e.g., tracing, “syscalls”) or to replace sure externally seen states (e.g., the instruction meter).

JIT compiler

The reminiscence format of the JIT code area

Management stream

Each BPF instruction is a legitimate goal tackle for a bounce or name. eBPF directions are 8 bytes, with one exception: load double phrase (LDDW), which is 16 bytes. Which means, with this one exception, each 8-byte boundary within the BPF code tackle house is a legitimate bounce goal.

Relative jumps can all the time be resolve earlier than runtime; they’ll both resolve at translation-time (for backward jumps) or be ‘fixed up’ in any case directions have emitted (for ahead jumps). Oblique calls, nevertheless, should resolved at runtime. Subsequently, RBPF retains a mapping from the instruction index to the host tackle in order that the situation of the already-translated goal instruction can regarded up when an oblique name happens.

JIT compiler: The instruction meter

Solana applications are design to run with a particular ‘compute budget’, which is basically the variety of eBPF directions that may execute earlier than this system exits. As a way to implement this restrict (on probably non-terminating applications). the JIT compiler emits further logic to trace the variety of directions that been execute. The instruction meter is finest describe in this comment, however it could actually summarize as follows:

  • The supply of every department is instrumented to account for the directions that had been executed within the linear sequence because the final replace and to document the department goal (the start of the following linear sequence of directions to execute).
  • If a conditional department will not be really taken, the updates to the instruction meter are undone.
  • Extra instruction meter checks are insert at sure thresholds in lengthy linear sequences of directions.

The instruction meter has been the supply of a number of bugs up to now (e.g., try pull request 203 and pull request 263).

Calls and “syscalls”

For normal eBPF calls throughout the identical program, RBPF retains a separate stack from the host (at present utilizing fixed-size stack frames), tracks the present name depth, and exits with an error if the decision depth exceeds its funds. Solana applications specifically additionally must invoke different contracts and work together with sure blockchain states. RBPF has a mechanism known as “syscalls” by which eBPF applications could make calls into Solana-specific helper capabilities applied in Rust.

Exceptions

The JIT compiler could exit early if it encounters quite a few unrecoverable runtime situations (resembling division by zero or invalid reminiscence entry). For the reason that verifier doesn’t try to trace register content material, most exceptions are caught at runtime fairly than at verification time. Exception handlers are designed to document the present exception data into an EbpfError enum after which proceed to the exit the subroutine (which returns again into Rust code).

JIT compiler: Safety mitigations

RBPF accommodates a couple of options that fall beneath the class of “machine code diversification” and serve to considerably hardenhe JIT compiler towards exploitation. Two of the options (launched final yr) are constant sanitization and instruction address randomization.

Fixed sanitization adjustments how immediates are load into registers within the emit code. Reasonably than emitting a typical x86 MOVABS instruction, which might include the unmodify bytes of the speedy, the speedy is as a substitute offset by a randomly generate key. At runtime, this secret’s fetch from reminiscence in a subsequent instruction and add in order that the vacation spot register accommodates the initially desired speedy.

Instruction tackle randomization provides no-op directions at random areas all through the emitted code. Each of those mitigations are intend to make code-reuse assaults tougher.

Porting RBPF to ARM64

Calling conference and register allocation

The JIT compiler wants to have the ability to name into Rust code, which can comply with the host’s calling conference. Fortunately, most platforms comply with the ARM software standard for the calling conference. Each Apple and Microsoft publish their very own ABI documentation, however they principally comply with the usual ARM64 documentation. I examined my implementation on M1 working macOS and on an emulated ARM64 digital machine via QEMU.

Be aware that ARM64’s further registers imply that even after mapping every eBPF register to a number register, there’s a substantial variety of further unuse host registers. I used a few of these further registers to carry further “scratch” values through the translation of extra advanced directions. Extra scratch values are sometimes useful since solely load and retailer directions can entry reminiscence in ARM64, which frequently leads to longer translations with extra short-term values.

Instruction-by-instruction translation

I wrote translations to ARM64 for every of the eBPF directions, modeled carefully after their x86 translations. The next is an instance of the present x86 and the brand new translated ARM64 code for 2 variants of the eBPF ADD instruction.

JIT compiler

The present x86 code

The translated ARM64 code

Be aware that ARM64’s fastened instruction measurement of 4 bytes means that you may’t encode each 32-bit speedy in a single instruction, and ARM64 ALU directions can encode solely a really restricted vary of speedy values. So some easy eBPF directions require a number of ARM64 directions (e.g., emit_load_immediate64 could emit a couple of instruction to maneuver the speedy into the scratch register), even when they require solely a single x86 instruction.

JIT compiler: Some surprises

The ARM64 ABI has a required stack alignment of 16-bytes on the time of any SP-relative entry; this alignment is suppose to enforced by {hardware}. QEMU doesn’t implement this alignment by default, however the Apple M1 does.

The subroutines (that are chargeable for exception dealing with, tackle translation, resolving oblique calls, and so on.) every have barely totally different conventions for his or her inputs and outputs, and these conventions should not effectively doc. Rewriting these subroutines appropriately in ARM64 was, by far, essentially the most time-consuming a part of this course of. I did finally doc a lot of my assumptions about these subroutines. These subroutines are additionally chargeable for some fairly advanced logic, together with tackle translation and instruction meter accounting.

Once I printed the ARM64 port, I made certain it was behind a feature-gate, jit-aarch64-not-safe-for-production. That is an intern venture goal to permit builders to make use of the JIT compiler, and it’s not prepared for manufacturing till it  obtain a radical peer evaluate.

My ARM64 port of RBPF is at present obtainable via the Trail of Bits fork or this pull request.

Winapi

The Home windows digital reminiscence APIs use VirtualAlloc and VirtualProtect in lieu of mmap and mprotect. For our functions, these are almost drop-in replacements—I simply needed to choose the permission and allocation choices that correspond most carefully to these utilized in mmap and mprotect.

JIT compiler: Calling conference

The Home windows x64 calling conference designates totally different registers as caller and callee-save. It additionally a further “shadow space” requirement through which callers are chargeable for leaving 32 bytes of house on the stack earlier than the decision (after any stack-resident arguments have pushed).

As with ARM64, Home windows help is behind a characteristic flag, jit-windows-not-safe-for-production.

A small, unexploitable bug

My ARM64 port of RBPF did uncover a small. Unexploitable uninitialized reminiscence bug that was current even within the present x86 JIT compiler. VTCAKAVSMoACE pointed out some warnings when working my ARM64 department beneath the LLVM reminiscence sanitizer (MSAN). I investigated these warnings and located the offender to be this perform:

fn emit_set_exception_kind<E: UserDefinedError>(jit: &mut JitCompiler, err: EbpfError<E>) {
    let err = End result::<u64, EbpfError<E>>::Err(err);
    let err_kind = unsafe { *(&err as *const _ as *const u64).offset(1) };
    ...
    emit_ins(jit, X86Instruction::store_immediate(OperandSize::S64, R10, X86IndirectAccess::Offset(8), err_kind as i64));
}

This perform takes an EbpfError worth because the second argument, strikes it right into a End result, after which makes use of unsafe code to seize bytes 8 via 16 out of the End result. These bytes correspond to the integer discriminant that determines which variant (error sort) the EbpfError is. No ensures are made by the Rust compiler in regards to the measurement or format of enums, except you add a repr attribute to the enum (like #[repr(u64)]).

The Rust compiler had determined that the EbpfError enum discriminant can be solely a u8, so the enum that’s handed to emit_set_exception_kind really had 7 bytes of uninitialized stack reminiscence that was being written into the JIT code area. Uninitialize (probably attacker-control) bytes which can be written into executable area will not be a bug by itself. However they partially defeat the aim of the code-reuse mitigations mentioned above.

I opened a pull request that provides #[repr(u64)]. For the reason that JIT compiler makes a further assumption about enum layouts (i.e., for End result within the Rust customary library), I additionally added exams that ought to detect whether or not the compiler ever adjustments the situation or measurement of the enum discriminant on sure sorts.

JIT compiler: Conclusion

Given how necessary the RBPF JIT compiler is to the Solana blockchain. We felt that it was necessary for the widest vary of builders to apply it to. No matter machine they’re utilizing for growth. Now, it’s potential for builders utilizing both M1 and Home windows machines to additionally use the JIT compiler throughout testing. Whereas the work nonetheless wants a peer evaluate, it could actually present in two pull requests on GitHub. Be at liberty to attempt it out!

Because of Anders Helsing for the implausible steering as I explored the internals of RBPF. And discovered the finer factors of each the ARM64 and Home windows x64 ABI.

This work reveals how Path of Bits is root in fixing Solana’s safety challenges. Constructing upon the deep Solana experience we’ve used to construct instruments we’ve got already launched to the general public. Not solely can we goal to make Solana as safe as potential. We need to make the instruments engineers use with. Solana equally as safe. Our final purpose with these efforts is to boost the safety stage for all the. Solana tasks that may constructed sooner or later.

Author: Path of Bits
Date: 2022-10-12 08:00:55

Source link

spot_imgspot_img

Subscribe

Related articles

spot_imgspot_img
Alina A, Toronto
Alina A, Torontohttp://alinaa-cybersecurity.com
Alina A, an UofT graduate & Google Certified Cyber Security analyst, currently based in Toronto, Canada. She is passionate for Research and to write about Cyber-security related issues, trends and concerns in an emerging digital world.

LEAVE A REPLY

Please enter your comment!
Please enter your name here