Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

bpf, x64: Fix a jit convergence issue

Daniel Hodges reported a jit error when playing with a sched-ext program.
The error message is:
unexpected jmp_cond padding: -4 bytes

But further investigation shows the error is actual due to failed
convergence. The following are some analysis:

...
pass4, final_proglen=4391:
...
20e: 48 85 ff test rdi,rdi
211: 74 7d je 0x290
213: 48 8b 77 00 mov rsi,QWORD PTR [rdi+0x0]
...
289: 48 85 ff test rdi,rdi
28c: 74 17 je 0x2a5
28e: e9 7f ff ff ff jmp 0x212
293: bf 03 00 00 00 mov edi,0x3

Note that insn at 0x211 is 2-byte cond jump insn for offset 0x7d (-125)
and insn at 0x28e is 5-byte jmp insn with offset -129.

pass5, final_proglen=4392:
...
20e: 48 85 ff test rdi,rdi
211: 0f 84 80 00 00 00 je 0x297
217: 48 8b 77 00 mov rsi,QWORD PTR [rdi+0x0]
...
28d: 48 85 ff test rdi,rdi
290: 74 1a je 0x2ac
292: eb 84 jmp 0x218
294: bf 03 00 00 00 mov edi,0x3

Note that insn at 0x211 is 6-byte cond jump insn now since its offset
becomes 0x80 based on previous round (0x293 - 0x213 = 0x80). At the same
time, insn at 0x292 is a 2-byte insn since its offset is -124.

pass6 will repeat the same code as in pass4. pass7 will repeat the same
code as in pass5, and so on. This will prevent eventual convergence.

Passes 1-14 are with padding = 0. At pass15, padding is 1 and related
insn looks like:

211: 0f 84 80 00 00 00 je 0x297
217: 48 8b 77 00 mov rsi,QWORD PTR [rdi+0x0]
...
24d: 48 85 d2 test rdx,rdx

The similar code in pass14:
211: 74 7d je 0x290
213: 48 8b 77 00 mov rsi,QWORD PTR [rdi+0x0]
...
249: 48 85 d2 test rdx,rdx
24c: 74 21 je 0x26f
24e: 48 01 f7 add rdi,rsi
...

Before generating the following insn,
250: 74 21 je 0x273
"padding = 1" enables some checking to ensure nops is either 0 or 4
where
#define INSN_SZ_DIFF (((addrs[i] - addrs[i - 1]) - (prog - temp)))
nops = INSN_SZ_DIFF - 2

In this specific case,
addrs[i] = 0x24e // from pass14
addrs[i-1] = 0x24d // from pass15
prog - temp = 3 // from 'test rdx,rdx' in pass15
so
nops = -4
and this triggers the failure.

To fix the issue, we need to break cycles of je <-> jmp. For example,
in the above case, we have
211: 74 7d je 0x290
the offset is 0x7d. If 2-byte je insn is generated only if
the offset is less than 0x7d (<= 0x7c), the cycle can be
break and we can achieve the convergence.

I did some study on other cases like je <-> je, jmp <-> je and
jmp <-> jmp which may cause cycles. Those cases are not from actual
reproducible cases since it is pretty hard to construct a test case
for them. the results show that the offset <= 0x7b (0x7b = 123) should
be enough to cover all cases. This patch added a new helper to generate 8-bit
cond/uncond jmp insns only if the offset range is [-128, 123].

Reported-by: Daniel Hodges <hodgesd@meta.com>
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
Link: https://lore.kernel.org/r/20240904221251.37109-1-yonghong.song@linux.dev
Signed-off-by: Alexei Starovoitov <ast@kernel.org>

authored by

Yonghong Song and committed by
Alexei Starovoitov
c8831bdb 23457b37

+52 -2
+52 -2
arch/x86/net/bpf_jit_comp.c
··· 64 64 return value <= 127 && value >= -128; 65 65 } 66 66 67 + /* 68 + * Let us limit the positive offset to be <= 123. 69 + * This is to ensure eventual jit convergence For the following patterns: 70 + * ... 71 + * pass4, final_proglen=4391: 72 + * ... 73 + * 20e: 48 85 ff test rdi,rdi 74 + * 211: 74 7d je 0x290 75 + * 213: 48 8b 77 00 mov rsi,QWORD PTR [rdi+0x0] 76 + * ... 77 + * 289: 48 85 ff test rdi,rdi 78 + * 28c: 74 17 je 0x2a5 79 + * 28e: e9 7f ff ff ff jmp 0x212 80 + * 293: bf 03 00 00 00 mov edi,0x3 81 + * Note that insn at 0x211 is 2-byte cond jump insn for offset 0x7d (-125) 82 + * and insn at 0x28e is 5-byte jmp insn with offset -129. 83 + * 84 + * pass5, final_proglen=4392: 85 + * ... 86 + * 20e: 48 85 ff test rdi,rdi 87 + * 211: 0f 84 80 00 00 00 je 0x297 88 + * 217: 48 8b 77 00 mov rsi,QWORD PTR [rdi+0x0] 89 + * ... 90 + * 28d: 48 85 ff test rdi,rdi 91 + * 290: 74 1a je 0x2ac 92 + * 292: eb 84 jmp 0x218 93 + * 294: bf 03 00 00 00 mov edi,0x3 94 + * Note that insn at 0x211 is 6-byte cond jump insn now since its offset 95 + * becomes 0x80 based on previous round (0x293 - 0x213 = 0x80). 96 + * At the same time, insn at 0x292 is a 2-byte insn since its offset is 97 + * -124. 98 + * 99 + * pass6 will repeat the same code as in pass4 and this will prevent 100 + * eventual convergence. 101 + * 102 + * To fix this issue, we need to break je (2->6 bytes) <-> jmp (5->2 bytes) 103 + * cycle in the above. In the above example je offset <= 0x7c should work. 104 + * 105 + * For other cases, je <-> je needs offset <= 0x7b to avoid no convergence 106 + * issue. For jmp <-> je and jmp <-> jmp cases, jmp offset <= 0x7c should 107 + * avoid no convergence issue. 108 + * 109 + * Overall, let us limit the positive offset for 8bit cond/uncond jmp insn 110 + * to maximum 123 (0x7b). This way, the jit pass can eventually converge. 111 + */ 112 + static bool is_imm8_jmp_offset(int value) 113 + { 114 + return value <= 123 && value >= -128; 115 + } 116 + 67 117 static bool is_simm32(s64 value) 68 118 { 69 119 return value == (s64)(s32)value; ··· 2281 2231 return -EFAULT; 2282 2232 } 2283 2233 jmp_offset = addrs[i + insn->off] - addrs[i]; 2284 - if (is_imm8(jmp_offset)) { 2234 + if (is_imm8_jmp_offset(jmp_offset)) { 2285 2235 if (jmp_padding) { 2286 2236 /* To keep the jmp_offset valid, the extra bytes are 2287 2237 * padded before the jump insn, so we subtract the ··· 2363 2313 break; 2364 2314 } 2365 2315 emit_jmp: 2366 - if (is_imm8(jmp_offset)) { 2316 + if (is_imm8_jmp_offset(jmp_offset)) { 2367 2317 if (jmp_padding) { 2368 2318 /* To avoid breaking jmp_offset, the extra bytes 2369 2319 * are padded before the actual jmp insn, so