[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_DISSECTOR
  • NETNS_BPF_SK_LOOKUP
  • NETNS_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;

그리고 typeNETNS_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이 이걸 버그라고 집어냈는지는 모르겠지만, 덕분에 새로운 것을 배울 수 있었다.