Skip to content

eBPF tail_call OOB (CVE-2022-2905)

The eBPF verifier's range analysis over-approximated the bpf_tail_call index, so a program could call bpf_tail_call with a key larger than the prog-array's max_entries; the x86 JIT then indexed bpf_array->ptr out of bounds, a slab out-of-bounds read into kernel heap.

Mechanism

Over-approximate edilmiş bir index safety kontrolünden neden kaçar

bpf_tail_call(ctx, &prog_array, index), bir eBPF program'ından bir BPF_MAP_TYPE_PROG_ARRAY map'inin index slot'unda saklanan başka bir program'a atlar. Arkadaki depo bpf_array->ptr, map oluşturulurken tam olarak max_entries slot için allocate edilir, dolayısıyla her erişim index < max_entries koşulunu sağlamalıdır.

bpf_tail_call iki şekilde compile edilir:

  • index known bir sabit değilse, kernel interpreter'a düşer; bu da açık bir runtime bound kontrolü yapar (if (index >= array->map.max_entries) return;).
  • Verifier index'in aralık içinde bir sabit olduğunu kanıtlarsa, x86 JIT verifier'ın kanıtına güvenerek runtime bound kontrolü olmadan bpf_array->ptr[index]'i index'leyen hızlı bir inline yol yayımlar.

Bug, o kanıttadır. Verifier'ın value-range analizi, gerçek değer kümesini over-approximate eden tnum ve min/max bounds kullanır. Index known bir sabit olduğunda verifier onu tnum_range(0, map->max_entries - 1) ve tnum_in(range, reg->var_off) ile doğruluyordu. Ama bir tnum range, kesin bir aralık değil bir bitmask superset'idir: tnum_range(0, 2), {0,1,2} yerine {0,1,2,3}'ü temsil eden 00XX verir. Bu yuvarlama yüzünden, aslında max_entries'ten büyük bir key "aralık içinde" sayılabiliyordu — yani verifier onun güvenli bir sabit index olduğuna inanıyordu. Dolayısıyla interpreter mode'a zorlamak (güvenli fallback) ya da program'ı reddetmek yerine, verifier onu kabul etti ve JIT'in kontrolsüz hızlı yolu yayımlamasına izin verdi. BRF yazarlarının deyişiyle: "calling bpf_tail_call with a key larger than the max_entries does not violate the semantic rules enforced by the verifier ... since the value range analysis mechanism in the verifier over-approximates the range, a key larger than the max_entries could be deemed within the range."

Runtime'da JIT'lenmiş kod key > max_entries ile bpf_array->ptr[key]'i hesaplar ve slab allocation'ının sonunun ötesinden bir bpf_prog * okur — kontrollü bir slab out-of-bounds read ve bir information-leak / potansiyel type-confusion primitive'i. Red Hat advisory'si bunu şöyle özetler: "A bpf_tail_call with a key larger than the max_entries of the map can cause an out-of-bound access when the x86 JIT compiler tries to index bpf_array->ptr using the invalid key."

CVE-2022-2905, kök nedenin bir tnum range-analizi kusuru olduğunu bulan BRF (the eBPF Runtime Fuzzer) tarafından keşfedildi. Fix commit'i a657182a5c51 ("bpf: Don't use tnum_range on array range checking for poke descriptors", Daniel Borkmann), Fixes: d2e4c1e6c294; sabit index'i artık doğrudan max_entries'e karşı karşılaştırır. v6.0-rc4'te fix'lenmiştir (backport'lar: stable 5.10.140 / 5.15.64 / 5.19.6). Etki information disclosure'dır (CVSS 5.5). Not: CVE-2022-49985 aynı patch için duplicate bir kayıttır — karıştırmayın.

Walkthrough

BRF'nin yayımladığı proof-of-concept minimaldir: küçük bir max_entries ile bir prog-array oluştur, sonra onun ötesinde bir index ile tail-call yap.

// Figure 7, BRF paper (simplified PoC of CVE-2022-2905)
DEFINE_BPF_MAP(map_0,
    BPF_MAP_TYPE_PROG_ARRAY,   // map type
    uint32_t,                  // key type
    uint32_t,                  // value type
    36                         // max_entries
);

SEC("cgroup/sock_create")
int func(struct bpf_sock *ctx) {
    bpf_tail_call(ctx, &map_0, 49);   // 49 > 36 == max_entries
    return 0;
}

1. Prog-array'i oluştur max_entries = 36 ile; kernel bpf_array->ptr'yi 36 bpf_prog * slot için allocate eder.

2. Out of bounds tail-call yap. Literal key 49, 36'yı aşar. Zafiyetli kernel'de verifier'ın over-approximate edilmiş aralığı 49'u "aralık içinde" sayar, dolayısıyla program reddedilmek ya da interpreter'a düşürülmek yerine kabul edilip JIT-compile edilir.

3. Runtime'da OOB read. Program çalıştığında, JIT'lenmiş hızlı yol bpf_array->ptr[49]'u index'ler ve allocation'ın sonunun 13 slot ötesinden bir pointer okur:

slab object: [ ptr[0] ptr[1] ... ptr[35] ] | <adjacent slab memory>
read index 49 ------------------------------------------^ OOB

4. Gözlemle / doğrula. Bir KASAN kernel'de erişim bir slab out-of-bounds read olarak raporlanır (CVE özeti "slab-out-of-bound read in bpf" ile uyumlu):

==================================================================
BUG: KASAN: slab-out-of-bounds in ... (bpf_tail_call / array_map ...)
Read of size 8 at addr ffff8881........ by task poc/PID
==================================================================

Leak'lenen pointer komşu kernel object'lerine işaret eder; heap grooming ile birleştirildiğinde bu bir info-leak (KASLR'ı etkisizleştirir) hâline gelir ya da OOB slot'u attacker-etkili bir bpf_prog-biçimli object tutacak şekilde yapılırsa, bir control-flow primitive'i olur.

Detection

  • Verifier-acceptance bug'ı: kötü niyetli bir program zafiyetli kernel'lerde hatasız load olur, dolayısıyla load zamanında tespit edin. Unprivileged context'lerden gelen BPF_PROG_LOAD'u denetleyin ve max_entries'e yakın/ üzerinde sabit key'lerle yapılan prog-array tail call'ları işaretleyin.
  • KASAN build'leri slab OOB'yi doğrudan trigger zamanında raporlar.
  • Yamalı kernel'ler program'ı reddeder ya da interpreter mode'a zorlar; daha önce kabul edilen bir program'ın aniden reddedilmeye kayması fix'i gösterir.

Mitigation

  • v6.0-rc4'e ya da fix'i içeren bir backport'a yamalayın (örneğin stable 5.19.6).
  • kernel.unprivileged_bpf_disabled=1 ayarlayarak eBPF program'ları load etmek için root / CAP_BPF gerektirin, böylece unprivileged attack surface'ı kaldırın.
  • Container'larda bpf syscall'ını seccomp ile bloklayın ve CAP_BPF/CAP_SYS_ADMIN'i drop edin.

References