|
| 1 | +--- |
| 2 | +simd: '0377' |
| 3 | +title: eBPF ISA compatibility |
| 4 | +authors: |
| 5 | + - Lucas Steuernagel (Anza) |
| 6 | + - Alexander Meißner (Anza) |
| 7 | +category: Standard |
| 8 | +type: Core |
| 9 | +status: Review |
| 10 | +created: 2025-10-09 |
| 11 | +feature: (fill in with feature key and github tracking issues once accepted) |
| 12 | +--- |
| 13 | + |
| 14 | +## Summary |
| 15 | + |
| 16 | +This SIMD introduces instruction set architecture (ISA) changes to make the |
| 17 | +sBPF virtual machine compatible with the latest existing version of eBPF ISA |
| 18 | +generated by its LLVM backend. |
| 19 | + |
| 20 | +It reverts past ISA changes, modifies the encoding of existing instructions |
| 21 | +and brings new instructions to the Solana virtual machine. |
| 22 | + |
| 23 | +## Motivation |
| 24 | + |
| 25 | +The eBPF target on the Rust compiler emits code by default for eBPFv1, whose |
| 26 | +only incompatibility with the Solana virtual machine is the `callx` |
| 27 | +instruction. Aiming to prioritize Solana programs and decrease their CU |
| 28 | +consumption, we want to be compatible with at least the current eBPF version |
| 29 | +(v3), which brings in new instructions. In order for that to be possible, we |
| 30 | +must modify our virtual machine to support eBPF integrally. |
| 31 | + |
| 32 | +Additionally, we are going to anticipate some eBPF v4 instructions that are |
| 33 | +beneficial for Solana and would decrease the update burden in case v4 becomes |
| 34 | +the new default configuration for the eBPF upstream LLVM target. |
| 35 | + |
| 36 | +## New Terminology |
| 37 | + |
| 38 | +The set containing these new instructions will form an sBPFv3 program. |
| 39 | + |
| 40 | +## Detailed Design |
| 41 | + |
| 42 | +### ELF Identification |
| 43 | + |
| 44 | +Programs containing the instructions mentioned in this SIMD must have the |
| 45 | +`0x03` value in the `e_flags` field of their header. |
| 46 | + |
| 47 | +### Revert SIMD-0166 |
| 48 | + |
| 49 | +SIMD-0166 must be reverted beginning with sBPFv3, since we will introduce a |
| 50 | +new design for dynamic stack frames that is closer to the eBPF code generation. |
| 51 | + |
| 52 | +### Revert SIMD-0173 |
| 53 | + |
| 54 | +All changes proposed in SIMD-173 will no longer take effect in sBPFv3. |
| 55 | +Consequently, the verifier must accept the following opcodes: |
| 56 | + |
| 57 | +- `0x18`, `0x00` (`LDDW`) |
| 58 | +- `0x72`, `0x71`, `0x73` (`STB`, `LDXB`, `STXB`) |
| 59 | +- `0x6A`, `0x69`, `0x6B` (`STH`, `LDXH`, `STXH`) |
| 60 | +- `0x62`, `0x61`, `0x63` (`STW`, `LDXW`, `STXW`) |
| 61 | +- `0x7A`, `0x79`, `0x7B` (`STDW`, `LDXDW`, `STXDW`) |
| 62 | +- `0xD4` (`LE`) |
| 63 | + |
| 64 | +The new opcodes introduced in SIMD-173 must be rejected in the verifier with `VerifierError::UnknownOpCode`: |
| 65 | + |
| 66 | +- the `HOR64` instruction (opcode `0xF7`) |
| 67 | +- the moved opcodes: |
| 68 | + - `0x27`, `0x2C`, `0x2F` (`STB`, `LDXB`, `STXB`) |
| 69 | + - `0x37`, `0x3C`, `0x3F` (`STH`, `LDXH`, `STXH`) |
| 70 | + - `0x87`, `0x8C`, `0x8F` (`STW`, `LDXW`, `STXW`) |
| 71 | + - `0x97`, `0x9C`, `0x9F` (`STDW`, `LDXDW`, `STXDW`) |
| 72 | + |
| 73 | +### Revert SIMD-0174 |
| 74 | + |
| 75 | +All changes proposed in SIMD-174 will no longer take effect in sBPFv3. |
| 76 | +Consequently, the verifier must accept the following opcodes: |
| 77 | + |
| 78 | +- the `MUL` instruction (opcodes `0x24`, `0x2C`, `0x27` and `0x2F`) |
| 79 | +- the `DIV` instruction (opcodes `0x34`, `0x3C`, `0x37` and `0x3F`) |
| 80 | +- the `MOD` instruction (opcodes `0x94`, `0x9C`, `0x97` and `0x9F`) |
| 81 | +- the `NEG` instruction (opcodes `0x84` and `0x87`) |
| 82 | + |
| 83 | +The verifier must reject programs and throw `VerifierError::UnknownOpCode` for |
| 84 | +programs that contain any of the following opcodes. |
| 85 | + |
| 86 | +- the `UHMUL64` instruction (opcode `0x36` and `0x3E`) |
| 87 | +- the `UDIV32` instruction (opcode `0x46` and `0x4E`) |
| 88 | +- the `UDIV64` instruction (opcode `0x56` and `0x5E`) |
| 89 | +- the `UREM32` instruction (opcode `0x66` and `0x6E`) |
| 90 | +- the `UREM64` instruction (opcode `0x76` and `0x7E`) |
| 91 | +- the `LMUL32` instruction (opcode `0x86` and `0x8E`) |
| 92 | +- the `LMUL64` instruction (opcode `0x96` and `0x9E`) |
| 93 | +- the `SHMUL64` instruction (opcode `0xB6` and `0xBE`) |
| 94 | +- the `SDIV32` instruction (opcode `0xC6` and `0xCE`) |
| 95 | +- the `SDIV64` instruction (opcode `0xD6` and `0xDE`) |
| 96 | +- the `SREM32` instruction (opcode `0xE6` and `0xEE`) |
| 97 | +- the `SREM64` instruction (opcode `0xF6` and `0xFE`) |
| 98 | + |
| 99 | +### Execution changes |
| 100 | + |
| 101 | +MOV32_REG (opcode `0x14`) must NOT perform sign extension. |
| 102 | + |
| 103 | +SUB32_IMM and SUB64_IMM must perform the operation `src = src - imm`. |
| 104 | + |
| 105 | +### Dynamic stack frames |
| 106 | + |
| 107 | +Aiming a closer compatibility to eBPF, the implementation of dynamic stack |
| 108 | +frames is going to change. |
| 109 | + |
| 110 | +The R10 register must continue to be the frame pointer, i.e. pointing to the |
| 111 | +highest address accessible in a function. As such, the stack will grow upwards. |
| 112 | + |
| 113 | +At the prologue of each function, there may be one ADD64 (opcode 0x07) |
| 114 | +instruction to adjust the frame pointer to its new position with a positive |
| 115 | +offset (`add64 R10, +imm`). When a function returns, the virtual machine will |
| 116 | +automatically restore the frame pointer register with the value used in the |
| 117 | +caller, so programs do not need to emit any instruction to adjust the frame |
| 118 | +pointer in the epilogue of each function. |
| 119 | + |
| 120 | +### JMP32 instruction class |
| 121 | + |
| 122 | +The JMP32 instruction class utilizes 32 bit wide operands for the same |
| 123 | +operations as the JMP class. |
| 124 | + |
| 125 | +The following opcodes must be allowed in the verifier and the virtual machine |
| 126 | +must implement the behavior described below for each one of them. |
| 127 | + |
| 128 | +- `JEQ32_IMM` -> opcode = `0x16` -> `pc += offset if dst as u32 == IMM as u32` |
| 129 | +- `JGT32_IMM` -> opcode = `0x26` -> `pc += offset if dst as u32 > IMM as u32` |
| 130 | +- `JGE32_IMM` -> opcode = `0x36` -> `pc += offset if dst as u32 >= IMM as u32` |
| 131 | +- `JSET32_IMM` -> opcode = `0x46` -> |
| 132 | + `pc += offset if (dst as u32 & IMM as u32) != 0` |
| 133 | +- `JNE32_IMM` -> opcode = `0x56` -> `pc += offset if dst as u32 != IMM as u32` |
| 134 | +- `JSGT32_IMM` -> opcode = `0x66` -> `pc += offset if dst as i32 > IMM as i32` |
| 135 | +- `JSGE32_IMM` -> opcode = `0x76` -> `pc += offset if dst as i32 > IMM as i32` |
| 136 | +- `JLT32_IMM` -> opcode = `0xa6` -> `pc += offset if dst as u32 < IMM as u32` |
| 137 | +- `JLE32_IMM` -> opcode = `0xb6` -> `pc += offset if dst as u32 <= IMM as u32` |
| 138 | +- `JSLT32_IMM` -> opcode = `0xc6` -> `pc += offset if dst as i32 < IMM as i32` |
| 139 | +- `JSLE32_IMM` -> opcode = `0xd6` -> `pc += offset if dst as i32 <= IMM as i32` |
| 140 | + |
| 141 | +- `JEQ32_REG` -> opcode = `0x1e` -> `pc += offset if dst as u32 == src as u32` |
| 142 | +- `JGT32_REG` -> opcode = `0x2e` -> `pc += offset if dst as u32 > src as u32` |
| 143 | +- `JGE32_REG` -> opcode = `0x3e` -> `pc += offset if dst as u32 >= src as u32` |
| 144 | +- `JSET32_REG` -> opcode = `0x4e` -> |
| 145 | + `pc += offset if (dst as u32 & src as u32) != 0` |
| 146 | +- `JNE32_REG` -> opcode = `0x56` -> `pc += offset if dst as u32 != src as u32` |
| 147 | +- `JSGT32_REG` -> opcode = `0x66` -> `pc += offset if dst as i32 > src as i32` |
| 148 | +- `JSGE32_REG` -> opcode = `0x76` -> `pc += offset if dst as i32 > src as i32` |
| 149 | +- `JLT32_REG` -> opcode = `0xa6` -> `pc += offset if dst as u32 < src as u32` |
| 150 | +- `JLE32_REG` -> opcode = `0xb6` -> `pc += offset if dst as u32 <= src as u32` |
| 151 | +- `JSLT32_REG` -> opcode = `0xc6` -> `pc += offset if dst as i32 < src as i32` |
| 152 | +- `JSLE32_REG` -> opcode = `0xd6` -> `pc += offset if dst as i32 <= src as i32` |
| 153 | + |
| 154 | +### SMOD and SDIV instructions |
| 155 | + |
| 156 | +The following opcodes must be allowed in the verifier for a sBPFv3 program and |
| 157 | +the following behavior must occur in the virtual machine. |
| 158 | + |
| 159 | +- `SMOD64_IMM` -> opcode = `0x97` -> `dst = dst as i64 % imm as i64` |
| 160 | +- `SMOD64_REG` -> opcode = `0x9f` -> `dst = dst as i64 % src as i64` |
| 161 | +- `SMOD32_IMM` -> opcode = `0x94` -> `dst = dst as i32 % imm as i32` |
| 162 | +- `SMOD32_REG` -> opcode = `0x9c` -> `dst = dst as i32 % src as i32` |
| 163 | +- `SDIV64_IMM` -> opcode = `0x37` -> `dst = dst as i64 / imm as i64` |
| 164 | +- `SDIV64_REG` -> opcode = `0x3f` -> `dst = dst as i64 / src as i64` |
| 165 | +- `SDIV32_IMM` -> opcode = `0x34` -> `dst = dst as i32 / imm as i32` |
| 166 | +- `SDIV32_REG` -> opcode = `0x3c` -> `dst = dst as i32 / src as i32` |
| 167 | + |
| 168 | +### Sign extended mov and sign extended load |
| 169 | + |
| 170 | +The verifier must accept the following instruction encodings for sign extended |
| 171 | +`mov` operations, and the virtual machine must implement the behavior detailed |
| 172 | +below for them. |
| 173 | + |
| 174 | +The existing `MOV64` and `MOV32` instructions were included in the list below |
| 175 | +only for comparison. |
| 176 | + |
| 177 | +- `MOV64` -> opcode = `0xbf`, offset = `0` -> `dst = src as i64` |
| 178 | + (existing instruction - only here for comparison) |
| 179 | +- `MOV64S8` -> opcode = `0xbf`, offset = `8` -> `dst = src as i8 as i64` |
| 180 | +- `MOV64S16` -> opcode = `0xbf`, offset = `16` -> `dst = src as i16 as i64` |
| 181 | +- `MOV64S32` -> opcode = `0xbf`, offset = `32` -> `dst = src as i32 as i64` |
| 182 | + |
| 183 | +- `MOV32` -> opcode = `0xbc`, offset = `0` -> `dst = src as u32` |
| 184 | + (existing instruction - only here for comparison) |
| 185 | +- `MOV32S8` -> opcode = `0xbc`, offset = `8` -> `dst = src as i8 as i32` |
| 186 | +- `MOV32S16` -> opcode = `0xbc`, offset = `16` -> `dst = src as i16 as i32` |
| 187 | + |
| 188 | + |
| 189 | +### Indirect jump |
| 190 | + |
| 191 | +The indirect jump instruction `jx` jumps to the instruction pointed by the |
| 192 | +address in the source register. In sum, the verifier must allow the following |
| 193 | +opcode and the runtime must implement the following behavior. |
| 194 | + |
| 195 | +- `jx` -> opcode = `0x0d` -> pc = `src` |
| 196 | + |
| 197 | +### callx encoding |
| 198 | + |
| 199 | +The encoding of callx must change so that the register containing the address |
| 200 | +to jump to is in the destination register. |
| 201 | + |
| 202 | +- `callx` -> opcode = `0x9d` -> pc = `dst` |
| 203 | + |
| 204 | + |
| 205 | +### Reinterpretation of LDDW as MOV and HOR |
| 206 | + |
| 207 | +The LDDW instruction consists of two 8-byte instruction frames, but consumes |
| 208 | +only one CU in the virtual machine. |
| 209 | + |
| 210 | +The virtual machine must now interpret the first half of LLDW as the opcode |
| 211 | +`0x18`, with the same behavior as `mov32 reg, imm` zero extending the |
| 212 | +immediate value. |
| 213 | + |
| 214 | +Likewise, the second half of LDDW must be interpreted as the opcode `0x00`, |
| 215 | +being a bitwise OR operation of the MSBs in the destination register. This |
| 216 | +instruction must be called `hor64 reg, imm`. |
| 217 | + |
| 218 | +The HOR instruction, however, must encode a destination register on which to |
| 219 | +operate. The encoding is as follows. |
| 220 | + |
| 221 | +- `HOR64` -> opcode = `0x00` -> `dst = dst | (imm << 32)` |
| 222 | + |
| 223 | +Consequently, we must charge two CUs for an LDDW execution. |
| 224 | + |
| 225 | +## Alternatives Considered |
| 226 | + |
| 227 | +We have considered diverging from the eBPF standard by introducing new opcodes |
| 228 | +and creating specific instructions to the Solana environment. We discarded |
| 229 | +such an approach to be compatible with the existing LLVM eBPF code generation. |
| 230 | + |
| 231 | +We are not adding `may_goto`, since it has an implicit condition implemented |
| 232 | +in the kernel, and does not yet have any path for code generation. We are also |
| 233 | +not supporting any of the eBPF atomic instructions, since the Solana virtual |
| 234 | +machine is single threaded. |
| 235 | + |
| 236 | +In eBPFv4, there are two other instructions that could be part of the Solana |
| 237 | +vendored virtual machine, but are not included in the proposal: `gotol` |
| 238 | +(opcode `0x06`) and `bswap` (opcode `0xd7`). |
| 239 | + |
| 240 | +`gotol` is an unconditional jump with a 32 bit offset. It does not replace the |
| 241 | +existing JA instruction (opcode 0x05), and is used only when the 16-bit offset |
| 242 | +from the JA can't be used for a jump. This situation appears only in very |
| 243 | +large functions, or in environments with aggressive inlining, and has not so |
| 244 | +far represented a problem for smart contracts. |
| 245 | + |
| 246 | +`bswap` supersedes LE (opcode 0xd4) and BE (opcode 0xdc) in eBPFv4, but |
| 247 | +otherwise behaves similarly. Byte swap is rarely used as an instruction in |
| 248 | +smart contracts, and so far the existing opcodes already fill up any needs. |
| 249 | + |
| 250 | +## Impact |
| 251 | + |
| 252 | +With these changes, and a patch to the aya bpf-linker, developers will be able |
| 253 | +to install the bpf-linker and use the existing rustup/cargo/rustc |
| 254 | +infrastructure to build their programs. |
| 255 | + |
| 256 | +## Security Considerations |
| 257 | + |
| 258 | +None |
| 259 | + |
0 commit comments