BPF/eBPF/XDP (BPF and XDP)
Linux 커널 BPF/eBPF 프로그래밍, XDP 패킷 처리, BPF 맵, CO-RE 종합 가이드.
BPF/eBPF 개요
eBPF(extended Berkeley Packet Filter)는 커널 내에서 안전하게 실행되는 프로그래밍 프레임워크입니다. 커널을 재컴파일하지 않고 네트워킹, 보안, 추적, 성능 분석 등의 기능을 확장할 수 있습니다.
XDP (eXpress Data Path)
XDP는 NIC 드라이버 수준에서 패킷을 처리하는 고성능 데이터 경로입니다. 전체 네트워크 스택을 거치지 않으므로 초저지연 패킷 처리가 가능합니다.
/* XDP program example (C for BPF) */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
SEC("xdp")
int xdp_drop_icmp(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *iph = (void *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return XDP_PASS;
if (iph->protocol == IPPROTO_ICMP)
return XDP_DROP; /* Drop ICMP packets */
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
BPF Maps
| 맵 유형 | 설명 | 용도 |
|---|---|---|
BPF_MAP_TYPE_HASH | 해시 테이블 | 키-값 저장 |
BPF_MAP_TYPE_ARRAY | 배열 | 인덱스 기반 접근 |
BPF_MAP_TYPE_PERCPU_HASH | Per-CPU 해시 | 고속 카운터 |
BPF_MAP_TYPE_RINGBUF | 링 버퍼 | 이벤트 스트리밍 |
BPF_MAP_TYPE_LRU_HASH | LRU 해시 | 연결 추적 |
BPF_MAP_TYPE_PROG_ARRAY | 프로그램 배열 | tail call |
CO-RE (Compile Once, Run Everywhere)
CO-RE는 BPF 프로그램을 한 번 컴파일하여 다양한 커널 버전에서 실행할 수 있게 합니다. BTF(BPF Type Format)를 활용하여 구조체 레이아웃 차이를 자동으로 조정합니다.
bpftool prog list로 현재 로드된 BPF 프로그램을, bpftool map list로 BPF 맵을 확인할 수 있습니다. bpftool prog dump xlated id <ID>로 JIT 컴파일된 코드를 볼 수 있습니다.
BPF Verifier
BPF verifier는 프로그램 로드 시 안전성을 검증합니다. 모든 경로가 종료되는지, 메모리 범위를 벗어나지 않는지, 무한 루프가 없는지 등을 정적으로 분석합니다.
/* Verifier가 검증하는 주요 사항 */
/* 1. 경계 검사: 모든 포인터 접근에 범위 확인 필요 */
if (data + sizeof(struct ethhdr) > data_end)
return XDP_PASS; /* 이 검사 없으면 verifier 거부 */
/* 2. 레지스터 상태 추적: R0-R10의 타입과 범위 */
/* 3. 루프 제한: bounded loop만 허용 (5.3+) */
for (int i = 0; i < 256; i++) { /* OK: bounded */
if (condition) break;
}
/* 4. 명령어 수 제한: 최대 1M instructions (5.2+) */
/* 5. 맵 접근: NULL 체크 필수 */
__u64 *val = bpf_map_lookup_elem(&my_map, &key);
if (val) /* NULL 체크 없으면 verifier 거부 */
*val += 1;
BPF 헬퍼 함수
| 헬퍼 | 용도 | 사용 가능 프로그램 |
|---|---|---|
bpf_map_lookup_elem() | 맵에서 값 조회 | 전체 |
bpf_map_update_elem() | 맵에 값 저장 | 전체 |
bpf_probe_read_kernel() | 커널 메모리 읽기 | kprobe, tracepoint |
bpf_get_current_pid_tgid() | 현재 프로세스 PID | kprobe, tracepoint |
bpf_ktime_get_ns() | 나노초 타임스탬프 | 전체 |
bpf_trace_printk() | 디버그 출력 | 전체 |
bpf_redirect() | 패킷 리다이렉트 | XDP, TC |
bpf_skb_store_bytes() | 패킷 데이터 수정 | TC |
bpf_ringbuf_output() | 링 버퍼로 이벤트 전송 | 전체 |
kprobe/tracepoint BPF 프로그램
/* kprobe: 커널 함수 진입 시 실행 */
SEC("kprobe/do_sys_openat2")
int BPF_KPROBE(trace_openat2, int dfd, const char *filename)
{
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_printk("PID %d (%s) opening file\n", pid, comm);
return 0;
}
/* tracepoint: 정적 트레이스포인트에 연결 */
SEC("tracepoint/sched/sched_process_exec")
int trace_exec(struct trace_event_raw_sched_process_exec *ctx)
{
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_printk("exec: %s\n", comm);
return 0;
}
BPF Ring Buffer
Ring Buffer는 BPF 프로그램에서 유저스페이스로 이벤트 데이터를 효율적으로 전송하는 메커니즘입니다. perf buffer보다 오버헤드가 낮고 메모리 효율이 좋습니다.
/* BPF 측: 이벤트 전송 */
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); /* 256KB */
} events SEC(".maps");
struct event {
u32 pid;
char comm[16];
};
SEC("tp/sched/sched_process_fork")
int trace_fork(void *ctx)
{
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e) return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0);
return 0;
}
libbpf와 BPF 스켈레톤
/* 유저스페이스: libbpf 스켈레톤 활용 */
#include "trace.skel.h" /* bpftool gen skeleton으로 생성 */
int main(void)
{
struct trace_bpf *skel;
skel = trace_bpf__open_and_load(); /* BPF 프로그램 로드 */
trace_bpf__attach(skel); /* hook에 연결 */
/* Ring buffer 폴링 */
struct ring_buffer *rb;
rb = ring_buffer__new(
bpf_map__fd(skel->maps.events),
handle_event, NULL, NULL);
while (!exiting)
ring_buffer__poll(rb, 100); /* 100ms 타임아웃 */
trace_bpf__destroy(skel);
}
TC (Traffic Control) BPF
TC BPF는 XDP보다 상위 계층에서 실행되며, 전체 sk_buff 구조체에 접근할 수 있어 더 풍부한 패킷 조작이 가능합니다.
# TC BPF 프로그램 로드
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj my_prog.o sec tc_ingress
tc filter add dev eth0 egress bpf da obj my_prog.o sec tc_egress
# 확인
tc filter show dev eth0 ingress
bpftool prog list
BPF 프로그램은 CAP_BPF (또는 CAP_SYS_ADMIN) 권한이 필요합니다. sysctl kernel.unprivileged_bpf_disabled=1이 설정된 환경에서는 root만 BPF 프로그램을 로드할 수 있습니다.
SmartNIC과 하드웨어 오프로드
SmartNIC(또는 DPU — Data Processing Unit)은 네트워크 처리를 NIC 자체의 프로세서/FPGA에서 수행하여 호스트 CPU를 해방시키는 차세대 네트워크 인프라 기술입니다.
SmartNIC 유형과 커널 지원
| 제조사 | 제품군 | 아키텍처 | 커널 드라이버 | XDP 지원 |
|---|---|---|---|---|
| NVIDIA (Mellanox) | ConnectX-6/7, BlueField-2/3 | ASIC + ARM SoC (DPU) | mlx5_core |
XDP native + TC flower HW offload |
| Netronome | Agilio CX | NFP (Network Flow Processor) | nfp |
XDP HW offload (BPF → NFP JIT) |
| Intel | E810 (IPU), Mount Evans | ASIC + FPGA | ice |
XDP native, ADQ (Application Device Queues) |
| Broadcom | Stingray PS1100 | ARM SoC | bnxt_en |
XDP native |
| Marvell | OCTEON 10 DPU | ARM SoC + HW accel | octeontx2 |
XDP native |
XDP 하드웨어 오프로드
# XDP 실행 모드 비교
# 1. Generic XDP — 모든 NIC에서 동작 (성능 낮음, 테스트/개발용)
ip link set dev eth0 xdpgeneric obj prog.o sec xdp
# 2. Native XDP — NIC 드라이버 지원 필요 (고성능)
ip link set dev eth0 xdp obj prog.o sec xdp
# 3. HW Offload XDP — SmartNIC에서 실행 (최고 성능, 호스트 CPU 0%)
ip link set dev eth0 xdpoffload obj prog.o sec xdp
# 현재 XDP 모드 확인
ip link show dev eth0
# ... xdp/id:42 ... (native)
# ... xdpoffload/id:42 ... (hw offload)
# 성능 비교 (64B 패킷, 단일 코어 기준)
# Generic XDP: ~5 Mpps
# Native XDP: ~24 Mpps
# HW Offload: ~40+ Mpps (NIC 와이어 레이트)
TC Flower 하드웨어 오프로드
# TC flower 규칙을 SmartNIC HW로 오프로드
# — 매칭/액션을 NIC의 eSwitch에서 처리
# switchdev 모드 설정 (eSwitch 활성화)
devlink dev eswitch set pci/0000:03:00.0 mode switchdev
# VF representor를 통한 플로우 오프로드
tc qdisc add dev enp3s0f0_0 ingress
tc filter add dev enp3s0f0_0 ingress protocol ip flower src_ip 10.0.0.0/24 dst_ip 192.168.1.0/24 action mirred egress redirect dev enp3s0f0_1 skip_sw # ← skip_sw: HW 전용 (소프트웨어 경로 스킵)
# 오프로드된 플로우 확인
tc -s filter show dev enp3s0f0_0 ingress
# in_hw ← 하드웨어에서 실행 중임을 표시
SmartNIC 커널 개발 시 고려사항
- BPF 명령어 제한 — HW offload 시 NIC 프로세서가 지원하는 BPF 명령어 서브셋만 사용 가능. 복잡한 맵 조회, 루프, 헬퍼 함수 일부가 제한됨
- 맵 동기화 — 오프로드된 BPF 맵은 NIC 메모리에 존재. 호스트에서 맵 업데이트 시 지연 발생 가능
- switchdev 모드 전환 — eSwitch 모드 변경 시 네트워크 순간 단절. 운영 중 변경 주의
- 펌웨어 호환성 — SmartNIC 펌웨어와 커널 드라이버 버전 호환성 확인 필수. 불일치 시 오프로드 실패
- 디버깅 어려움 — HW 오프로드된 로직은 tcpdump/BPF trace로 관찰 불가.
skip_sw대신skip_hw로 먼저 S/W 테스트 후 전환 - Fallback 전략 — HW offload 실패 시 자동으로 SW 경로로 fallback되지만 성능 급락. 모니터링 필수
- CT offload — conntrack 오프로드(
tc ct action)는 제한적. 지원되는 연결 수, 타임아웃 동작이 SW와 다를 수 있음
SmartNIC과 DPDK/AF_XDP 연동
- AF_XDP — XDP에서 사용자 공간으로 제로카피 패킷 전달.
XDP_REDIRECT→ AF_XDP 소켓으로 커널 바이패스 수준의 성능 - DPDK + SmartNIC — 완전한 커널 바이패스. PMD(Poll Mode Driver)로 NIC 직접 제어. 최대 성능이지만 커널 네트워크 스택 기능(방화벽, 라우팅) 사용 불가
- 하이브리드 — SmartNIC eSwitch에서 fast path(TC flower offload)와 slow path(커널 스택) 분리. 일반 트래픽은 HW, 예외는 SW 처리
BPF/eBPF 주요 보안 취약점 사례
BPF/eBPF는 커널 내에서 사용자 정의 코드를 실행하는 강력한 메커니즘이지만, Verifier 우회, JIT 취약점, 투기적 실행 등 다양한 공격 벡터가 존재합니다. 아래는 실제로 보고된 주요 보안 취약점 사례와 그 대응 방법입니다.
BPF Verifier 우회 취약점 (CVE-2021-3490)
이 취약점은 BPF Verifier가 ALU32 연산에서 비트 연산(AND, OR, XOR)의 결과 범위를 잘못 계산하는 데서 발생합니다. Verifier는 각 레지스터의 가능한 값 범위를 tnum(tracked number) 구조체로 추적하는데, 비트 연산 후 tnum 값과 min/max 경계가 불일치하면 실제로는 불가능한 값 범위를 허용하게 됩니다.
/* 취약점을 유발하는 BPF 코드 패턴 (개념 예시) */
/* Verifier가 r0의 범위를 [0, 1]로 추적하지만 */
/* 실제 ALU32 비트 연산 후 범위가 확장될 수 있음 */
BPF_ALU64_IMM(BPF_MOV, BPF_REG_0, 0),
BPF_ALU64_IMM(BPF_OR, BPF_REG_0, 0x7FFFFF00),
/* Verifier는 r0 ≤ 0x7FFFFFFF로 추적하지만 */
/* tnum 분석과 범위 분석의 불일치로 */
/* out-of-bounds 맵 접근이 허용됨 */
BPF_ALU32_IMM(BPF_AND, BPF_REG_0, idx),
/* 이 시점에서 Verifier의 범위 계산 오류 발생 */
BPF_STX_MEM(BPF_DW, BPF_REG_MAP, BPF_REG_0, 0),
/* 맵 경계 밖의 커널 메모리에 쓰기 가능 */
수정 패치에서는 ALU 연산의 tnum 검증을 강화하여 비트 연산 후 tnum.value와 tnum.mask가 min/max 경계와 일관성을 유지하도록 했습니다.
# 비특권 BPF 비활성화 (재부팅 시 초기화)
sysctl -w kernel.unprivileged_bpf_disabled=1
# 영구 설정 (/etc/sysctl.d/99-bpf.conf)
kernel.unprivileged_bpf_disabled = 1
BPF JIT Spraying (CVE-2020-8835)
이 취약점은 BPF JIT 컴파일러가 ALU32 연산을 처리할 때 32비트 값의 상위 32비트를 적절히 truncation하지 않아 발생합니다. Verifier는 32비트 연산의 결과를 올바르게 추적하지만, JIT가 생성한 네이티브 코드에서는 64비트 레지스터의 상위 비트가 보존되어 실제 값이 Verifier가 추적한 범위를 벗어나게 됩니다.
/* CVE-2020-8835: 32비트/64비트 불일치 악용 패턴 */
/* 1단계: 64비트 레지스터에 큰 값 로드 */
r1 = 0x100000001 /* 상위 32비트: 1, 하위 32비트: 1 */
/* 2단계: ALU32 연산 — Verifier는 하위 32비트만 추적 */
w1 += 0 /* 32비트 ADD: Verifier는 w1=1로 추적 */
/* 그러나 JIT 코드는 상위 비트를 truncate하지 않음 */
/* 실제 r1 = 0x100000001 (Verifier 추적 범위 초과) */
/* 3단계: 이 불일치를 이용한 OOB 접근 */
r2 = map_ptr /* BPF 맵 포인터 */
r2 += r1 /* Verifier: map + 1 (안전) */
/* 실제: map + 0x100000001 (OOB!) */
*(r2) = 0 /* 커널 메모리 arbitrary write */
수정 패치에서는 JIT 컴파일러가 모든 ALU32 연산 후에 명시적으로 상위 32비트를 zero-extend하도록 하여 레지스터 값이 Verifier의 추적 범위와 일치하도록 했습니다.
# JIT 상태 확인
cat /proc/sys/net/core/bpf_jit_enable
# 0: 비활성화 (인터프리터)
# 1: 활성화 (기본값, fallback 가능)
# 2: 디버깅 모드 (JIT 이미지 덤프)
# CONFIG_BPF_JIT_ALWAYS_ON=y 시 인터프리터 완전 제거
# JIT 비활성화 불가 — 항상 네이티브 코드 실행
Spectre v1과 BPF (CVE-2019-7308)
Spectre v1(bounds check bypass) 공격은 CPU의 분기 예측기를 훈련시켜, 배열 경계 검사가 완료되기 전에 투기적으로 범위 밖의 데이터를 로드하게 만듭니다. BPF 프로그램은 사용자가 작성한 코드를 커널 내에서 실행하므로, 투기적 실행의 영향을 받는 패턴을 쉽게 만들 수 있었습니다.
/* Spectre v1 공격 패턴 (BPF 맵 배열 접근) */
/* 공격자가 제어하는 인덱스 값 */
r0 = *(u32 *)(r1 + offsetof(ctx, data)) /* 외부 입력 */
/* Verifier는 이 분기를 확인하여 r0 < array_size 보장 */
if r0 >= array_size goto out
/* 그러나 CPU는 투기적으로 분기를 무시하고 실행 */
/* r0가 array_size를 초과하는 값으로 투기적 로드 */
r1 = map_base + r0 * 8 /* 투기적 OOB 포인터 계산 */
r2 = *(u64 *)(r1) /* 투기적 OOB 로드 — 커널 메모리 읽기 */
/* 캐시 사이드 채널을 통해 r2 값 추출 */
r3 = probe_array + r2 * 64 /* 캐시 라인 크기 단위 접근 */
r4 = *(u64 *)(r3) /* 캐시에 r2 값의 흔적을 남김 */
수정 패치에서는 Verifier가 투기적 실행에 취약한 패턴을 탐지하여 lfence 명령(직렬화 배리어)을 삽입합니다. 또한 포인터 산술에 대한 BPF_ALU64_REG sanitization을 도입하여, 투기적 실행 경로에서도 포인터가 허용된 범위를 벗어나지 않도록 마스킹합니다.
/* 수정 후: Verifier가 삽입하는 방어 코드 */
if r0 >= array_size goto out
/* Verifier가 자동 삽입하는 sanitization */
r0 &= array_mask /* 투기적 실행에서도 범위 제한 */
/* x86에서는 lfence 삽입으로 투기적 실행 직렬화 */
r1 = map_base + r0 * 8 /* 이제 투기적 OOB 불가 */
r2 = *(u64 *)(r1)
BPF 맵 경쟁 조건 사례
BPF_MAP_TYPE_ARRAY는 고정 크기 배열로, 개별 요소에 대한 읽기/쓰기가 원자적이지 않습니다. 멀티 코어 환경에서 동일 요소를 동시에 업데이트하면 torn read/write가 발생하여 일부만 업데이트된 값을 읽을 수 있습니다. BPF_MAP_TYPE_HASH에서는 해시 테이블 resize 과정에서 element가 손실되거나 중복되는 문제가 보고되었습니다.
/* 문제: torn read/write가 발생하는 맵 접근 */
struct stats {
__u64 packets;
__u64 bytes;
};
/* CPU 0과 CPU 1이 동시에 같은 키의 stats를 업데이트 */
struct stats *val = bpf_map_lookup_elem(&stats_map, &key);
if (val) {
val->packets += 1; /* 비원자적: read-modify-write 경쟁 */
val->bytes += pkt_len; /* packets과 bytes 사이에 다른 CPU가 끼어들 수 있음 */
/* 결과: 카운터 손실 또는 packets/bytes 불일치 */
}
bpf_spin_lock을 사용하면 맵 요소에 대한 동기화된 접근이 가능합니다. 커널 5.1부터 지원되며, 맵 value 구조체에 struct bpf_spin_lock 필드를 포함시켜야 합니다.
/* 해결책 1: bpf_spin_lock으로 동기화 */
struct stats_locked {
struct bpf_spin_lock lock;
__u64 packets;
__u64 bytes;
};
struct stats_locked *val = bpf_map_lookup_elem(&stats_map, &key);
if (val) {
bpf_spin_lock(&val->lock);
val->packets += 1;
val->bytes += pkt_len;
bpf_spin_unlock(&val->lock);
}
/* 해결책 2: per-CPU 맵으로 락 없는 병렬 접근 */
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 256);
__type(key, __u32);
__type(value, struct stats);
} stats_percpu SEC(".maps");
/* 각 CPU가 독립적인 복사본에 접근 — 락 불필요 */
struct stats *val = bpf_map_lookup_elem(&stats_percpu, &key);
if (val) {
val->packets += 1; /* 경쟁 없음: CPU별 독립 메모리 */
val->bytes += pkt_len;
}
/* 사용자 공간에서 모든 CPU 값 합산 */
/* bpf_map_lookup_elem()은 ncpus개의 값을 반환 */
__u64 total_packets = 0;
for (int i = 0; i < ncpus; i++)
total_packets += percpu_vals[i].packets;
- BPF_MAP_TYPE_ARRAY / HASH — 공유 맵. 다중 CPU에서 동시 업데이트 시
bpf_spin_lock필요 - BPF_MAP_TYPE_PERCPU_ARRAY / PERCPU_HASH — CPU별 독립 복사본. 락 없이 안전하지만 사용자 공간에서 합산 필요
- __sync_fetch_and_add() — 단순 카운터에 대해 원자적 증가. 구조체 전체 동기화에는 부적합
- BPF_MAP_TYPE_HASH resize — 해시 맵의 동적 확장 중 요소 접근은 RCU로 보호되지만, 대량 삽입 시 성능 저하 주의