Repository: cilium/ebpf
Pull request #1608 (2024-11-11 11:06:38)

features: add HaveV4ISA probe for checking ISA v4 support in the kernel
This PR introduces the `haveV4ISA` probe and `HaveV4ISA` API to check in the running kernel if instructions of the v4 ISA are supported. The upstream commit used as reference is 1f9a1ea821ff ("bpf: Support new sign-extension load insns"). The probes tests the new long jump given by `BPF_JMP32 | BPF_JA`.

I'm attempting to submit an identical patch to bpftool, so that `bpftool feature probe` will output also ISAv4 support.

smagnani96: CI failing on [arm64](https://github.com/cilium/ebpf/actions/runs/12197582232/job/34027622012?pr=1608) with `bpf_jit: unknown opcode 06`.
smagnani96: > This fixes CI, but not the test. `TestHaveV4ISA` should return `ebpf.ErrNotSupported` on kernels <6.6, it looks like `probeProgram` receives EOPNOTSUP or ENOTSUPP from the verifier instead of the typical EINVAL when hitting unknown instructions on arm64. Please investigate the source of that deviation. This test should automatically skip on arm64 version 6.4 and succeed on 6.8.

Thanks Timo for the feedback :pray: 
I verified that in this case the error code returned is [sys.ENOTSUPP](https://github.com/cilium/ebpf/blob/main/internal/sys/syscall.go#L14).
Would like to add an intermediate commit to catch this in https://github.com/cilium/ebpf/blob/main/features/prog.go#L47, what do you think?
smagnani96: > This fixes CI, but not the test. `TestHaveV4ISA` should return `ebpf.ErrNotSupported` on kernels <6.6, it looks like `probeProgram` receives EOPNOTSUP or ENOTSUPP from the verifier instead of the typical EINVAL when hitting unknown instructions on arm64. Please investigate the source of that deviation. This test should automatically skip on arm64 version 6.4 and succeed on 6.8.

Thanks Timo for the feedback 🙏
I verified that in this case the error code returned is [sys.ENOTSUPP](https://github.com/cilium/ebpf/blob/main/internal/sys/syscall.go#L14).
I'm adding an intermediate commit to catch this in https://github.com/cilium/ebpf/blob/main/features/prog.go#L47, what do you think?
ti-mo: 
> ~I'm adding an intermediate commit to catch this in https://github.com/cilium/ebpf/blob/main/features/prog.go#L47, what do you think?~ I changed just the probe to verify the returned error, and turn it into ebpf.ErrNotSupported in case of a sys.ENOTSUPP. That error is returned on arm64 kernels prior to ISA v4 support (<=v6.6), I was able to reproduce with an EC2 Amazon Linux 2023 (kernel v6.1).

Thanks! How come the behaviour is different between x64 and arm64?
smagnani96: > Thanks! How come the behaviour is different between x64 and arm64?

Can't precisely tell the root source yet, I'm still trying to understand differences between codepath in x86 and arm64.
Thanks to @rgo3 for providing ideas, let me report our so-far analysis here.

On arm64, we can find the `bpf_jit: invalid opcode` error at `build_insn()` https://elixir.bootlin.com/linux/v6.4.16/source/arch/arm64/net/bpf_jit_comp.c#L1302-L1307.

```c
default:
		pr_err_once("unknown opcode %02x\n", code);
		return -EINVAL;
	}
```

That being sad, tracking back the call to `build_insns`, the returned -EINVAL can get changed into ENOTSUPP here https://elixir.bootlin.com/linux/v6.4.16/source/kernel/bpf/verifier.c#L17544-L17548:

```c
func[i] = bpf_int_jit_compile(func[i]);
if (!func[i]->jited) {
	err = -ENOTSUPP;
	goto out_free;
}
```

However, on x86, there's a similar same check for unknown BPF instructions https://elixir.bootlin.com/linux/v6.4.16/source/arch/x86/net/bpf_jit_comp.c#L1815-L1823:

```c
default:
	/*
	 * By design x86-64 JIT should support all BPF instructions.
	 * This error will be seen if new instruction was added
	 * to the interpreter, but not to the JIT, or if there is
	 * junk in bpf_prog.
	 */
	pr_err("bpf_jit: unknown opcode %02x\n", insn->code);
	return -EINVAL;
}
```

So that's why there's still confusion on why the two returned error codes differ (ENOTSUPP on arm64, EINVAL on x86).
The function `bpf_int_jit_compile`  differs between arm64 and x86 both in structure and  concept. For instance, I'm seeing that for x86 the JIT does multiple passes (`#define MAX_PASSES 20`) while for arm64 it doesn't.
smagnani96: [offtopic]: let me know if we want to drop 1st commit (upgrade ubuntu arm64 runner) in this PR.
ti-mo: > [offtopic]: let me know if we want to drop 1st commit (upgrade ubuntu arm64 runner) in this PR.

Yes please, let's bump it separately. We can bump all runners in lockstep, also x86.
borkmann: If JIT fails, it should always return -ENOTSUPP. These are the locations:
- https://elixir.bootlin.com/linux/v6.4.16/source/kernel/bpf/verifier.c#L17544-L17548
- https://elixir.bootlin.com/linux/v6.4.16/source/kernel/bpf/core.c#L2204

The -EINVAL is JIT internal and does not get propagated outside of JIT. JIT will just return un-JITed program back to the verifier/core and then depending on whether interpreter is compiled out or not this gets propagated to the user.
smagnani96: > If JIT fails, it should always return -ENOTSUPP. These are the locations:
> 
> * https://elixir.bootlin.com/linux/v6.4.16/source/kernel/bpf/verifier.c#L17544-L17548
> * https://elixir.bootlin.com/linux/v6.4.16/source/kernel/bpf/core.c#L2204
> 
> The -EINVAL is JIT internal and does not get propagated outside of JIT. JIT will just return un-JITed program back to the verifier/core and then depending on whether interpreter is compiled out or not this gets propagated to the user.

Thanks Daniel. I looked also that codepath yesterday and yet it is strange to be the following behavior.
Adding a print in our probeProgram https://github.com/cilium/ebpf/blob/main/features/prog.go#L41 leads to the following :thinking: 

```go
fmt.Println(errors.Is(err, sys.ENOTSUPP),  errors.Is(err, unix.EINVAL))
// on arm64
true false
// on x86
false true
```
rgo3: @smagnani96 What is the verifier log when we receive `EINVAL` on x86? That might help us figuring out where exactly the EINVAL gets produced.
borkmann: > @smagnani96 What is the verifier log when we receive `EINVAL` on x86? That might help us figuring out where exactly the EINVAL gets produced.

This + tracing return codes from the verifier to see where EINVAL gets propagated.
smagnani96: > > @smagnani96 What is the verifier log when we receive `EINVAL` on x86? That might help us figuring out where exactly the EINVAL gets produced.
> 
> This + tracing return codes from the verifier to see where EINVAL gets propagated.

Apologize for the mismatch of kernel versions, seems I can't run arm64 VMs with `lvh`, so I'm using what AWS offers.
Don't fully understand why in this case I see for x86 the same error I was seeing in the dmesg of the arm64 runner https://github.com/cilium/ebpf/pull/1608#issuecomment-2523064187.

However, talking from my little expertise, this seems that x86 doesn't know (yet) that instruction at all (https://elixir.bootlin.com/linux/v6.4.16/source/arch/x86/net/bpf_jit_comp.c#L1815-L1823), while arm64 somehow does.
Might be related to arm64 using fixed-length instruction set?

### x86

`Linux civil-jennet 6.4.0-060400-generic #202306271339 SMP PREEMPT_DYNAMIC Tue Jun 27 14:26:34 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux`

Error: invalid argument
Log: 
```bash
func#0 @0
unknown opcode 06
verification time 5 usec
stack depth 0
processed 0 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
```

### arm64

`Linux XXXXXX 6.1.115-126.197.amzn2023.aarch64 #1 SMP Tue Nov  5 17:36:31 UTC 2024 aarch64 aarch64 aarch64 GNU/Linux`

Error: operation not supported

```bash
func#0 @0
0: R1=ctx(off=0,imm=0) R10=fp0
0: (b7) r0 = 0                        ; R0_w=0
1: (15) if r0 == 0x1 goto pc+1
last_idx 1 first_idx 0
regs=1 stack=0 before 0: (b7) r0 = 0
1: R0_w=0
2: (06) if w0 jmp 0x1 goto pc+0
4: (95) exit
verification time 31 usec
stack depth 0
processed 4 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
```
borkmann: The error on x86-64 is not from the BPF JIT, but from the verifier instead: https://elixir.bootlin.com/linux/v6.4.16/source/kernel/bpf/verifier.c#L16750

Patch:
diff --git a/features/misc.go b/features/misc.go
index 0c4e1a2..c039020 100644
--- a/features/misc.go
+++ b/features/misc.go
@@ -1,9 +1,12 @@
 package features
 
 import (
+	"errors"
+
 	"github.com/cilium/ebpf"
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/internal"
+	"github.com/cilium/ebpf/internal/sys"
 )
 
 // HaveLargeInstructions probes the running kernel if more than 4096 instructions
@@ -62,7 +65,7 @@ func HaveV2ISA() error {
 }
 
 var haveV2ISA = internal.NewFeatureTest("v2 ISA", func() error {
-	return probeProgram(&ebpf.ProgramSpec{
+	err := probeProgram(&ebpf.ProgramSpec{
 		Type: ebpf.SocketFilter,
 		Instructions: asm.Instructions{
 			asm.Mov.Imm(asm.R0, 0),
@@ -71,6 +74,11 @@ var haveV2ISA = internal.NewFeatureTest("v2 ISA", func() error {
 			asm.Return().WithSymbol("exit"),
 		},
 	})
+	// This sometimes bubbles up from the JIT on aarch64.
+	if errors.Is(err, sys.ENOTSUPP) {
+		return ebpf.ErrNotSupported
+	}
+	return err
 }, "4.14")
 
 // HaveV3ISA probes the running kernel if instructions of the v3 ISA are supported.
@@ -83,7 +91,7 @@ func HaveV3ISA() error {
 }
 
 var haveV3ISA = internal.NewFeatureTest("v3 ISA", func() error {
-	return probeProgram(&ebpf.ProgramSpec{
+	err := probeProgram(&ebpf.ProgramSpec{
 		Type: ebpf.SocketFilter,
 		Instructions: asm.Instructions{
 			asm.Mov.Imm(asm.R0, 0),
@@ -92,4 +100,36 @@ var haveV3ISA = internal.NewFeatureTest("v3 ISA", func() error {
 			asm.Return().WithSymbol("exit"),
 		},
 	})
+	// This sometimes bubbles up from the JIT on aarch64.
+	if errors.Is(err, sys.ENOTSUPP) {
+		return ebpf.ErrNotSupported
+	}
+	return err
 }, "5.1")
+
+// HaveV4ISA probes the running kernel if instructions of the v4 ISA are supported.
+//
+// Upstream commit 1f9a1ea821ff ("bpf: Support new sign-extension load insns").
+//
+// See the package documentation for the meaning of the error return value.
+func HaveV4ISA() error {
+	return haveV4ISA()
+}
+
+var haveV4ISA = internal.NewFeatureTest("v4 ISA", func() error {
+	err := probeProgram(&ebpf.ProgramSpec{
+		Type: ebpf.SocketFilter,
+		Instructions: asm.Instructions{
+			asm.Mov.Imm(asm.R0, 0),
+			asm.JEq.Imm(asm.R0, 1, "error"),
+			asm.LongJump("exit"),
+			asm.Mov.Imm(asm.R0, 1).WithSymbol("error"),
+			asm.Return().WithSymbol("exit"),
+		},
+	})
+	// This sometimes bubbles up from the JIT on aarch64.
+	if errors.Is(err, sys.ENOTSUPP) {
+		return ebpf.ErrNotSupported
+	}
+	return err
+}, "6.6")


Test patch:
diff --git a/features/misc_test.go b/features/misc_test.go
index 95fa0e0..7e26103 100644
--- a/features/misc_test.go
+++ b/features/misc_test.go
@@ -21,3 +21,7 @@ func TestHaveV2ISA(t *testing.T) {
 func TestHaveV3ISA(t *testing.T) {
 	testutils.CheckFeatureTest(t, HaveV3ISA)
 }
+
+func TestHaveV4ISA(t *testing.T) {
+	testutils.CheckFeatureTest(t, HaveV4ISA)
+}