[Linux Kernel eBPF] Finding bugs with LLM - 1
동기
LLM으로 버그를 찾는 게 유의미하다는 이야기를 듣고, 언젠가 나도 해봐야지 하며 미뤄뒀던 일을 이번에 해봤다.
경과
안전장치가 우회되지 않을까 걱정돼 AGENTS.md와 프롬프트를 열심히 작성했는데, 의외로 쉽게 우회할 수 있었다.
OpenCode에 GPT-5.4 high를 물리고 리눅스 커널 전체를 던져주니, 한 10분쯤 생각하더니 버그 몇 개를 뱉었다.
LLM이 버그라고 주장하는 코드 net_namespace.c
// kernel/bpf/net_namespace.c
int netns_bpf_prog_attach(const union bpf_attr *attr, struct bpf_prog *prog)
{
struct bpf_prog_array *run_array;
enum netns_bpf_attach_type type;
struct bpf_prog *attached;
struct net *net;
int ret;
if (attr->target_fd || attr->attach_flags || attr->replace_bpf_fd)
return -EINVAL;
type = to_netns_bpf_attach_type(attr->attach_type);
if (type < 0)
return -EINVAL;
net = current->nsproxy->net_ns;
mutex_lock(&netns_bpf_mutex);
/* Attaching prog directly is not compatible with links */
if (!list_empty(&net->bpf.links[type])) {
ret = -EEXIST;
goto out_unlock;
}
switch (type) {
case NETNS_BPF_FLOW_DISSECTOR:
ret = flow_dissector_bpf_prog_attach_check(net, prog);
break;
default:
ret = -EINVAL;
break;
}
if (ret)
goto out_unlock;
attached = net->bpf.progs[type];
if (attached == prog) {
/* The same program cannot be attached twice */
ret = -EINVAL;
goto out_unlock;
}
run_array = rcu_dereference_protected(net->bpf.run_array[type],
lockdep_is_held(&netns_bpf_mutex));
if (run_array) {
WRITE_ONCE(run_array->items[0].prog, prog);
} else {
run_array = bpf_prog_array_alloc(1, GFP_KERNEL);
if (!run_array) {
ret = -ENOMEM;
goto out_unlock;
}
run_array->items[0].prog = prog;
rcu_assign_pointer(net->bpf.run_array[type], run_array);
}
net->bpf.progs[type] = prog;
if (attached)
bpf_prog_put(attached);
out_unlock:
mutex_unlock(&netns_bpf_mutex);
return ret;
}
이 함수에서 ret이 초기화되지 않았고, 그 때문에 syscall의 반환값이 쓰레기값이 될 수 있다는 게 LLM의 주장이었다.
처음에는 나도 코드를 대충 보고 맞는 줄 알고, 검증을 어떻게 하면 좋을지 물어봤다. 이런 종류의 버그는 KMSAN으로 잡을 수 있다는 것도 알게 되었다.
KMSAN을 켠 커널을 빌드하고, 이 함수가 트리거되는 selftest(flow_dissector_reattach.c)를 실행했다.
테스트는 잘만 통과했다. 😅
코드를 더 살펴보니 왜 이게 버그가 아닌지 알게 되었다.
깨달음
// include/linux/bpf-netns.h
static inline enum netns_bpf_attach_type
to_netns_bpf_attach_type(enum bpf_attach_type attach_type)
{
switch (attach_type) {
case BPF_FLOW_DISSECTOR:
return NETNS_BPF_FLOW_DISSECTOR;
case BPF_SK_LOOKUP:
return NETNS_BPF_SK_LOOKUP;
default:
return NETNS_BPF_INVALID;
}
}
to_netns_bpf_attach_type 함수가 있다. 이 함수는 아래 enum 중 하나를 반환한다.
NETNS_BPF_FLOW_DISSECTORNETNS_BPF_SK_LOOKUPNETNS_BPF_INVALID
type = to_netns_bpf_attach_type(attr->attach_type);
그 결과가 type 변수에 들어간다.
switch (type) {
case NETNS_BPF_FLOW_DISSECTOR:
ret = flow_dissector_bpf_prog_attach_check(net, prog);
break;
default:
ret = -EINVAL;
break;
}
if (ret)
goto out_unlock;
그리고 type이 NETNS_BPF_FLOW_DISSECTOR일 경우에는 flow_dissector_bpf_prog_attach_check()의 결과값이 ret에 담긴다.
아닐 경우에는 -EINVAL이 담긴다.
// net/core/flow_dissector.c
int flow_dissector_bpf_prog_attach_check(struct net *net,
struct bpf_prog *prog)
{
enum netns_bpf_attach_type type = NETNS_BPF_FLOW_DISSECTOR;
if (net == &init_net) {
/* BPF flow dissector in the root namespace overrides
* any per-net-namespace one. When attaching to root,
* make sure we don't have any BPF program attached
* to the non-root namespaces.
*/
struct net *ns;
for_each_net(ns) {
if (ns == &init_net)
continue;
if (rcu_access_pointer(ns->bpf.run_array[type]))
return -EEXIST;
}
} else {
/* Make sure root flow dissector is not attached
* when attaching to the non-root namespace.
*/
if (rcu_access_pointer(init_net.bpf.run_array[type]))
return -EEXIST;
}
return 0;
}
위 코드에서 flow_dissector_bpf_prog_attach_check()는 정상적으로 값을 반환한다. 결국 ret은 초기화되지 않아도 문제가 없다는 뜻이다.
소감
KMSAN에 대해 알게 되었다.
커널 코드는 만만하지 않다.
LLM은 아직 멍청하다. GPT가 멍청한 것일 수도...
심지어 크로스체크도 했지만 왜 LLM이 이걸 버그라고 집어냈는지는 모르겠지만, 덕분에 새로운 것을 배울 수 있었다.