Skip to content

Commit cef54cf

Browse files
authored
Merge pull request #135 from neuvector/test_bpf_versions
fix: solve some verifier issues and introduce a bpf CI
2 parents b088ccf + 86df41c commit cef54cf

11 files changed

Lines changed: 385 additions & 113 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: 'bpfvalidator'
2+
description: 'Run eBPF programs against a range of kernels.'
3+
4+
inputs:
5+
arch:
6+
description: 'Architecture to test (e.g., amd64, arm64)'
7+
required: true
8+
type: string
9+
args:
10+
description: 'Command line to pass to bpfvalidator'
11+
required: true
12+
type: string
13+
fail_on_validation:
14+
description: 'fail the action if validation fails'
15+
required: false
16+
default: true
17+
type: boolean
18+
skip_dependencies:
19+
description: 'skip installing dependencies'
20+
required: false
21+
default: false
22+
type: boolean
23+
24+
outputs:
25+
report:
26+
description: "Report of the validation"
27+
value: ${{ steps.run-validator.outputs.report }}
28+
outcome:
29+
description: "Outcome of the validation"
30+
value: ${{ steps.run-validator.outputs.outcome }}
31+
32+
runs:
33+
using: "composite"
34+
steps:
35+
# we hardcode the vng version here, since dependencies and build process could change among different versions, we shouldn't let the user choose the version
36+
- name: Install deps
37+
if: inputs.skip_dependencies != 'true'
38+
shell: bash
39+
run: |
40+
sudo apt update
41+
sudo apt install -y git qemu-system udev virtiofsd
42+
git clone --single-branch --branch v1.36 --depth 1 --recurse-submodules https://github.com/arighi/virtme-ng.git
43+
cd virtme-ng/
44+
BUILD_VIRTME_NG_INIT=1 pip3 install . --break-system-packages
45+
echo "$HOME/.local/bin" >> $GITHUB_PATH
46+
47+
# Try to enable KVM if available
48+
- name: KVM support
49+
shell: bash
50+
continue-on-error: true
51+
run: |
52+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
53+
sudo udevadm control --reload-rules
54+
sudo udevadm trigger --name-match=kvm
55+
sudo apt install -y qemu-kvm kmod
56+
57+
# we offer 2 possible ways to consume the output of bpfvalidator:
58+
# 1. print it directly in the action log
59+
# 2. set it as an output variable
60+
- name: Run bpfvalidator
61+
# we don't want the action to fail immediately if bpfvalidator fails.
62+
# so we remove `-eo pipefail` from the shell option.
63+
# see https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
64+
shell: bash --noprofile --norc {0}
65+
id: run-validator
66+
working-directory: ${{ github.action_path }}
67+
run: |
68+
wget https://github.com/Andreagit97/bpfvalidator/releases/download/v0.3.0/bpfvalidator_0.3.0_linux_${{ inputs.arch }}.tar.gz
69+
tar -xvf bpfvalidator_0.3.0_linux_${{ inputs.arch }}.tar.gz
70+
./bpfvalidator --out_path=/tmp/report.txt ${{ inputs.args }}
71+
outcome=$?
72+
echo "outcome=$outcome" >> $GITHUB_OUTPUT
73+
echo "report=/tmp/report.txt" >> $GITHUB_OUTPUT
74+
cat /tmp/report.txt
75+
if [ $outcome -ne 0 ] && [ "${{ inputs.fail_on_validation }}" == "true" ]; then
76+
exit $outcome
77+
fi

.github/workflows/bpf_test.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: bpf tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
name: test-${{ matrix.arch }}
10+
runs-on: ${{ (matrix.arch == 'arm64' && 'ubuntu-24.04-arm') || 'ubuntu-24.04' }}
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
arch: [amd64]
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
18+
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
19+
with:
20+
go-version-file: "go.mod"
21+
- name: Install system dependencies for eBPF build
22+
run: |
23+
sudo apt-get update
24+
sudo apt-get install -y \
25+
build-essential \
26+
libelf-dev \
27+
clang \
28+
llvm \
29+
libbpf-dev
30+
- name: Generate eBPF code
31+
run: |
32+
make generate-ebpf
33+
go test -c ./internal/bpf/... -o tester
34+
- name: Validate bpf tests using bpfvalidator
35+
uses: ./.github/actions/bpfvalidator
36+
with:
37+
args: |
38+
--config=$GITHUB_WORKSPACE/bpfvalidator-${{ matrix.arch }}-config.yaml --cmd="$GITHUB_WORKSPACE/tester -test.v"
39+
arch: ${{ matrix.arch }}

bpf/d_path_resolution.h

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44
// kernel's max dentry name length that is 255
55
// (https://elixir.bootlin.com/linux/v5.10/source/include/uapi/linux/limits.h#L12) + 1 for the `/`
66
#define MAX_COMPONENT_LEN 256
7-
// Max iterations when unrolling loops
8-
#define UNROLL_PATH_ITERATIONS 128
9-
// Max iterations when looping paths
10-
#define LOOP_PATH_ITERATIONS 2048
7+
// Max iterations when looping paths, we can reach at least 1024 but the verification time
8+
// increases, so for now we keep it conservative, and moreover 512 should be more than enough.
9+
#define FALLBACK_PATH_ITERATIONS 512
10+
// With numeric code iterators we have no limits.
11+
#define PATH_ITERATIONS 2048
1112

1213
#define DELETED_STRING " (deleted)"
1314

1415
#define SAFE_PATH_LEN(x) (x) & (MAX_PATH_LEN - 1)
1516
#define SAFE_PATH_ACCESS(x) (x) & (MAX_PATH_LEN * 2 - 1)
1617
#define SAFE_COMPONENT_ACCESS(x) (x) & (MAX_COMPONENT_LEN - 1)
1718

19+
extern int bpf_iter_num_new(struct bpf_iter_num *it, int start, int end) __ksym __weak;
20+
1821
struct path_read_data {
1922
struct dentry *root_dentry;
2023
struct vfsmount *root_mnt;
@@ -93,10 +96,6 @@ static __always_inline long path_read(struct path_read_data *data) {
9396
return 0;
9497
}
9598

96-
static long path_read_loop(__u32 index, void *data) {
97-
return path_read(data);
98-
}
99-
10099
// this method is inspired by Tetragon https://github.com/cilium/tetragon/pull/90
101100
// but simplified and reworked in light of our specific use case
102101
static __always_inline int bpf_d_path_approx(const struct path *path, char *buf) {
@@ -134,11 +133,25 @@ static __always_inline int bpf_d_path_approx(const struct path *path, char *buf)
134133
struct mount,
135134
mnt); // container_of comes from bpf_helpers.h and it is already adapted for CO-RE
136135

137-
if(bpf_core_enum_value_exists(enum bpf_func_id, BPF_FUNC_loop)) {
138-
bpf_loop(LOOP_PATH_ITERATIONS, path_read_loop, (void *)&data, 0);
136+
if(bpf_ksym_exists(bpf_iter_num_new)) {
137+
// Numeric code iterators are available from kernel 6.4
138+
// (https://docs.ebpf.io/linux/kfuncs/bpf_iter_num_new/) so we check if the kfunc is
139+
// available.
140+
// `bpf_repeat` is a macro defined by libbpf that uses `bpf_iter_num_new` under
141+
// the hood.
142+
// The initial implementation used `bpf_loop`, but this is not so handy to use with CO-RE,
143+
// you can find more info here
144+
// https://lore.kernel.org/bpf/CAGQdkDt9zyQwr5JyftXqL=OLKscNcqUtEteY4hvOkx2S4GdEkQ@mail.gmail.com/T/#u
145+
// and here https://github.com/falcosecurity/libs/pull/2027#issuecomment-2568997393
146+
// TL;DR; we need 2 ebpf programs, one with `bpf_loop` on kernels >= 5.13 and another
147+
// without it on older kernels.
148+
bpf_repeat(PATH_ITERATIONS) {
149+
if(path_read(&data)) {
150+
break;
151+
}
152+
}
139153
} else {
140-
#pragma unroll
141-
for(int i = 0; i < UNROLL_PATH_ITERATIONS; ++i) {
154+
for(int i = 0; i < FALLBACK_PATH_ITERATIONS; ++i) {
142155
if(path_read(&data)) {
143156
break;
144157
}

bpf/main.c

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ extern int LINUX_KERNEL_VERSION __kconfig;
2424
struct {
2525
__uint(type, BPF_MAP_TYPE_HASH);
2626
__uint(max_entries, TRACKER_MAP_MAX_ENTRIES);
27+
__uint(map_flags, BPF_F_NO_PREALLOC);
2728
__type(key, __u64); /* cgroup id */
2829
__type(value, __u64); /* tracker cgroup id */
2930
} cgtracker_map SEC(".maps");
@@ -426,11 +427,6 @@ static __always_inline u16 string_padded_len(u16 len) {
426427

427428
if(len <= STRING_MAPS_SIZE_6)
428429
return STRING_MAPS_SIZE_6;
429-
430-
if(LINUX_KERNEL_VERSION < KERNEL_VERSION(5, 11, 0)) {
431-
return STRING_MAPS_SIZE_7;
432-
}
433-
434430
if(len <= STRING_MAPS_SIZE_7)
435431
return STRING_MAPS_SIZE_7;
436432
if(len <= STRING_MAPS_SIZE_8)
@@ -441,13 +437,8 @@ static __always_inline u16 string_padded_len(u16 len) {
441437
}
442438

443439
static __always_inline int string_map_index(u16 padded_len) {
444-
if(padded_len < STRING_MAPS_SIZE_5)
440+
if(padded_len < STRING_MAPS_SIZE_5) {
445441
return (padded_len / STRING_MAPS_KEY_INC_SIZE) - 1;
446-
447-
if(LINUX_KERNEL_VERSION < KERNEL_VERSION(5, 11, 0)) {
448-
if(padded_len == STRING_MAPS_SIZE_6)
449-
return 6;
450-
return 7;
451442
}
452443

453444
switch(padded_len) {
@@ -515,6 +506,7 @@ int BPF_PROG(enforce_cgroup_policy, struct linux_binprm *bprm) {
515506

516507
// Only 5.11+ kernels support hash key lengths > 512 bytes
517508
// https://github.com/cilium/tetragon/commit/834b5fe7d4063928cf7b89f61252637d833ca018
509+
// From now on, we are sure that evt->path_len is <= STRING_MAPS_SIZE_7 if on <5.11
518510
if(LINUX_KERNEL_VERSION < KERNEL_VERSION(5, 11, 0)) {
519511
if(evt->path_len > STRING_MAPS_SIZE_7) {
520512
bpf_printk("Path length %d exceeds max supported length", evt->path_len);

bpf/string_maps.h

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

3-
#define POLICY_STR_OUTER_MAX_ENTRIES 1
3+
// we will decrese the number of entries in userspace if the map is not used (<5.11)
4+
#define POLICY_STR_OUTER_MAX_ENTRIES 65536
45
#define POLICY_STR_INNER_MAX_ENTRIES 1
56

67
/* Taken and adapted from https://github.com/cilium/tetragon/pull/1408
@@ -48,18 +49,6 @@
4849
#define STRING_MAPS_SIZE_9 (2048)
4950
#define STRING_MAPS_SIZE_10 (4096)
5051

51-
// todo!: we want to compile only once so we should avoid the ifdef. We should probably not create
52-
// maps > 8. Test it, if it is feasible
53-
//
54-
// #ifdef __V511_BPF_PROG
55-
// #define STRING_MAPS_SIZE_7 (512 + 2)
56-
// #define STRING_MAPS_SIZE_8 (1024 + 2)
57-
// #define STRING_MAPS_SIZE_9 (2048 + 2)
58-
// #define STRING_MAPS_SIZE_10 (4096 + 2)
59-
// #else
60-
// #define STRING_MAPS_SIZE_7 (512)
61-
// #endif
62-
6352
#define DEFINE_POLICY_STR_HASH_OF_MAPS(N) \
6453
struct { \
6554
__uint(type, BPF_MAP_TYPE_HASH_OF_MAPS); \
@@ -115,10 +104,3 @@ static __always_inline void* get_policy_string_map(int index, u64* policy_id) {
115104
}
116105
return 0;
117106
}
118-
119-
// todo!: same as before for `__V511_BPF_PROG`
120-
// #ifdef __V511_BPF_PROG
121-
// DEFINE_POLICY_STR_HASH_OF_MAPS(8)
122-
// DEFINE_POLICY_STR_HASH_OF_MAPS(9)
123-
// DEFINE_POLICY_STR_HASH_OF_MAPS(10)
124-
// #endif

bpfvalidator-amd64-config.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Number of parallel VMs to run
2+
parallel: 4
3+
# kernel versions to test
4+
kernel_versions:
5+
- v5.10.247
6+
- v5.15.197
7+
- v6.1.159
8+
- v6.6.119
9+
- v6.12.62
10+
- v6.17.12
11+
- v6.18.1

internal/bpf/manager.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ import (
66
"log/slog"
77

88
"github.com/cilium/ebpf"
9+
"github.com/cilium/ebpf/asm"
10+
"github.com/cilium/ebpf/features"
11+
"github.com/cilium/ebpf/link"
912
"github.com/cilium/ebpf/rlimit"
1013
"github.com/neuvector/runtime-enforcer/internal/cgroups"
14+
"github.com/neuvector/runtime-enforcer/internal/kernels"
15+
1116
"golang.org/x/sync/errgroup"
1217
)
1318

@@ -17,6 +22,9 @@ import (
1722

1823
const (
1924
loadTimeConfigBPFVar = "load_time_config"
25+
policyMap8Name = "pol_str_maps_8"
26+
policyMap9Name = "pol_str_maps_9"
27+
policyMap10Name = "pol_str_maps_10"
2028
)
2129

2230
const (
@@ -53,11 +61,58 @@ type Manager struct {
5361
monitoringEventChan chan ProcessEvent
5462
}
5563

64+
func probeEbpfFeatures() error {
65+
// For now known requirements are:
66+
// - BPF_MAP_TYPE_RINGBUF
67+
// - tracing prog with attach type BPF_MODIFY_RETURN
68+
69+
// Check for BPF_MAP_TYPE_RINGBUF
70+
if err := features.HaveMapType(ebpf.RingBuf); err != nil {
71+
return fmt.Errorf("BPF_MAP_TYPE_RINGBUF not supported: %w", err)
72+
}
73+
74+
// Check for BPF_MODIFY_RETURN attach type
75+
// Today there is no an helper function for attach type BPF_MODIFY_RETURN so we do it by hand.
76+
prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
77+
Name: "probe_fmodret",
78+
Type: ebpf.Tracing,
79+
Instructions: asm.Instructions{
80+
asm.Mov.Imm(asm.R0, 0),
81+
asm.Return(),
82+
},
83+
AttachType: ebpf.AttachModifyReturn,
84+
License: "MIT",
85+
AttachTo: "security_bprm_creds_for_exec",
86+
})
87+
if err != nil {
88+
return err
89+
}
90+
defer prog.Close()
91+
92+
link, err := link.AttachTracing(link.TracingOptions{
93+
Program: prog,
94+
})
95+
if err != nil {
96+
return err
97+
}
98+
err = link.Close()
99+
if err != nil {
100+
return err
101+
}
102+
103+
return nil
104+
}
105+
56106
func NewManager(logger *slog.Logger, enableLearning bool, eBPFLogLevel ebpf.LogLevel) (*Manager, error) {
57107
if err := rlimit.RemoveMemlock(); err != nil {
58108
return nil, fmt.Errorf("failed to remove memlock: %w", err)
59109
}
60110

111+
logger.Info("Probing eBPF features...")
112+
if err := probeEbpfFeatures(); err != nil {
113+
return nil, fmt.Errorf("failure during eBPF feature probing: %w", err)
114+
}
115+
61116
spec, err := loadBpf()
62117
if err != nil {
63118
return nil, fmt.Errorf("failed to load BPF spec: %w", err)
@@ -78,6 +133,25 @@ func NewManager(logger *slog.Logger, enableLearning bool, eBPFLogLevel ebpf.LogL
78133
"cgrp_v1_subsys_idx", conf.Cgrpv1SubsysIdx,
79134
"debug_mode", conf.DebugMode)
80135

136+
// Only kernels >= 5.11 support hash key lengths > 512 bytes
137+
// https://github.com/cilium/tetragon/commit/834b5fe7d4063928cf7b89f61252637d833ca018
138+
// so we reduce the key size for older kernels, these maps won't be used anyway
139+
if kernels.CurrVersionIsLowerThan("5.11") {
140+
for _, mapName := range []string{policyMap8Name, policyMap9Name, policyMap10Name} {
141+
policyMap, ok := spec.Maps[mapName]
142+
if !ok {
143+
return nil, fmt.Errorf("map %s not found in spec", mapName)
144+
}
145+
// Entries should be already set to 1 in the spec, but just in case
146+
policyMap.MaxEntries = 1
147+
if policyMap.InnerMap == nil {
148+
return nil, fmt.Errorf("map %s is not a hash of maps", mapName)
149+
}
150+
// this is the max key size supported on older kernels
151+
policyMap.InnerMap.KeySize = stringMapSize7
152+
}
153+
}
154+
81155
// We just load the objects here so that we can pass the maps to other components but we don't load ebpf progs yet
82156
objs := bpfObjects{}
83157
opts := &ebpf.CollectionOptions{

0 commit comments

Comments
 (0)