Enabling BBR on Redmi K80 via a Kernel Module

Device: Redmi K80 (zorn / 24117RK2CC)
System: HyperOS OS3.0.309.0.WOKCNXM / Android 16
Kernel: 6.1.138-android14-11-g965475777129-mi
Root: SukiSU Ultra 4.3.0 (LKM mode)

Result: BBR and fq are now up and running on this device, persisting across reboots. No kernel flashing, no waiting for Xiaomi to release the kernel source, and none of the screen flickering issues seen with GKI.

This post documents the entire process: how to confirm BBR is missing from the kernel, why a standard insmod fails, how to track down the matching source tree and compiler, how to avoid instant reboots upon loading, and finally how to package it into an auto-starting KernelSU module.

First, What Is BBR?

BBR is a TCP congestion control algorithm introduced by Google in 2016. Traditional algorithms like cubic rely on packet loss to detect congestion: the moment packets start dropping, cubic drastically cuts its sending rate. BBR takes a different approach—it estimates the bottleneck bandwidth and round-trip propagation time of the link, then paces packet transmission according to that model.

This difference is most noticeable on weak, high-latency links with some random packet loss. BBR generally keeps sender throughput higher while keeping queues short, preventing latency from piling up. That said, it is not a “speed hack”: peak bandwidth won’t magically increase out of thin air, and download speeds depend largely on the server’s congestion control algorithm and the link itself.

Android kernels don’t always ship with BBR compiled in. Many OEM configurations only keep reno and cubic to save build size and reduce behavioral variables. That is exactly the case on this K80.

Checking Kernel Support

First, check the currently available congestion control algorithms:

$ cat /proc/sys/net/ipv4/tcp_available_congestion_control
reno cubic

$ cat /proc/sys/net/ipv4/tcp_congestion_control
cubic

Next, check the kernel configuration:

$ zcat /proc/config.gz | grep TCP_CONG
CONFIG_TCP_CONG_CUBIC=y
CONFIG_DEFAULT_TCP_CONG="cubic"
# CONFIG_TCP_CONG_ADVANCED is not set

CONFIG_TCP_CONG_ADVANCED is not enabled, so BBR was never compiled at all. There is no tcp_bbr.ko under /vendor/lib/modules, and no bbr symbols in the kernel symbol table either.

In other words, it’s not a matter of a toggle being left off—the algorithm simply isn’t in the kernel.

Why Not Just Build a Full Kernel?

The standard approach would be to compile a full kernel with BBR enabled and flash it to the boot partition. There are two problems with that here.

First, Xiaomi hasn’t released the source code for 6.1.138-mi. MiCode only has the older bsp-zorn-v-oss (6.1.68 / Android 15), with no branch corresponding to Android 16.

Second, flashing a generic GKI or third-party kernel breaks the HyperOS display stack. I tested both a generic GKI 6.1.145 and a third-party zorn 6.1.176 kernel on this phone; both booted, but the screen flickered constantly while the vendor HWC/DRM repeatedly spammed COMMIT failed: 11. At the time, it looked like the vendor display modules were tightly coupled to the exact kernel version, meaning any kernel swap would break the display.

2026-11-05 Correction: That assessment was only half right. The flickering wasn’t because “newer kernels are inherently incompatible,” but rather because a DRM validation check introduced in stable 6.1.141 conflicted with possible_clones in zorn’s vendor msm_drm. Reverting that check fixed the display on 6.1.176. See “6.1.176: Display Compatibility and Dual BBR” at the end of this post for details.

So while building a full kernel seemed like a dead end at the time, it later proved to be fixable rather than impossible.

The Third Way: Loading a Module at Runtime

Even without replacing the full kernel, the stock kernel itself supports loadable modules. If we can compile a tcp_bbr.ko and load it at runtime, we don’t need to touch the boot partition or risk disturbing the display stack.

The catch is that standard insmod will reject the module.

There are three checks between a module and the kernel:

  1. vermagic: The kernel version string embedded in the module at build time must match the running kernel;
  2. MODVERSIONS: If the kernel has CONFIG_MODVERSIONS enabled, every external symbol referenced by the module carries a CRC checksum that the kernel verifies one by one;
  3. Symbol export: Symbols referenced by the module must exist in the kernel’s export table.

This phone’s kernel has CONFIG_MODVERSIONS=y, and the OEM didn’t provide Module.symvers, so a self-compiled module won’t have matching CRCs. Its vermagic will also mismatch because it lacks the -mi suffix.

A direct insmod will fail at the first or second check.

What ksud insmod Does

ksud insmod in SukiSU 4.3.0 doesn’t just blindly call init_module. It includes a userspace module loader that roughly does the following:

  1. Reads the .ko and parses undefined symbols in the ELF;
  2. Resolves the address of each symbol via the kernel symbol query interface (UAPI 6) or /proc/kallsyms;
  3. Rewrites the symbol table entries to SHN_ABS with their values set to the real addresses;
  4. Calls init_module;
  5. If the kernel rejects the module due to a vermagic mismatch, reads the expected vermagic from the kernel log, patches the string inside the .ko, and retries.

Once the symbols are rewritten as absolute addresses, the kernel no longer checks the export table or CRCs during module loading, and the vermagic is patched to match the device’s actual string. That’s how it manages to load “foreign” .ko files.

vpnhide_kmod.ko on the phone is a ready-made example. Its vermagic says 6.1.166-dirty, yet it loads and works fine on this 6.1.138-mi kernel.

Keep in mind, however, that ksud insmod only bypasses symbol and version checks. If struct layouts or CFI hashes don’t match, the module will still crash the kernel—often causing an instant reboot upon loading before any logs can be saved.

Hurdle 1: The Source Must Be ACK

My first build of the module used upstream stable 6.1.138 from kernel.org. It compiled cleanly, but when I compared the device BTF against the module BTF, every single struct size was off:

Struct Device BTF stable 6.1.138 Diff
sock 856 768 88
inet_sock 1088 984 104
inet_connection_sock 1536 1392 144
tcp_sock 2368 2200 168
module 1088 1024 64

The kernel version string is 6.1.138-android14-11-g965475777129-mi. There is more information packed in here than just “6.1.138”:

  • 6.1.138 is the upstream stable version;
  • -android14-11 indicates the android14-6.1 branch of the Android Common Kernel (ACK);
  • -g965475777129 is the commit hash of the build tree;
  • -mi represents Xiaomi’s own patches.

ACK is not mainline. It includes Android-specific patches and adds extra fields to kernel structs:

  • android_kabi_reserved*, controlled by CONFIG_ANDROID_KABI_RESERVE;
  • android_oem_data1, controlled by CONFIG_ANDROID_VENDOR_OEM_DATA;
  • Backported ACK fields like sk_wait_pending;
  • btf_data_size / btf_data inside struct module.

tcp_sock was shifted by a full 168 bytes. When BBR tried to read snd_cwnd, delivered, or app_limited, it would hit completely wrong memory offsets. A bug like this might not crash immediately, waiting instead to trigger on a retransmission or RTT sample.

So the source tree had to be switched to an official ACK tag. I used the GKI release tag from the TUNA mirror:

git fetch --depth 1 origin refs/tags/android14-6.1.138_r00
git checkout FETCH_HEAD

Verify the version:

head -5 Makefile
# VERSION = 6
# PATCHLEVEL = 1
# SUBLEVEL = 138

The g965475777129 hash in uname -r is from Xiaomi’s internal build tree and does not exist in the public ACK repository. Searching public repos for that hash won’t turn up anything.

Hurdle 2: Matching the Compiler to the Kernel

The device configuration contains this line:

CONFIG_CC_VERSION_TEXT="Android (10087095, +pgo, +bolt, +lto, -mlgo,
based on r487747c) clang version 17.0.2 ..."

And the kernel also enables:

CONFIG_CFI_CLANG=y
# CONFIG_CFI_PERMISSIVE is not set
CONFIG_LTO_CLANG_THIN=y

BBR’s function pointers are called indirectly by the kernel through struct tcp_congestion_ops. At indirect call sites, kCFI checks the type hash at the target function’s entry point. Because CFI_PERMISSIVE is not set, a hash mismatch causes an immediate kernel panic.

This means you can’t just pick any random compiler. It’s best to use the exact same AOSP clang r487747c (17.0.2) that built the kernel.

You can do a sparse checkout from TUNA’s platform/prebuilts/clang/host/linux-x86 repository:

git clone --filter=blob:none --no-checkout --depth 1 \
https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/prebuilts/clang/host/linux-x86.git clangpre
cd clangpre
git fetch --depth 1 origin android14-release
git sparse-checkout init --cone
git sparse-checkout set clang-r487747c
git checkout FETCH_HEAD
export PATH=$PWD/clang-r487747c/bin:$PATH

This directory is about 2.3GB.

I also compared the kCFI hashes generated by clang 17.0.2 and 18.1.3 for the function prototypes in tcp_congestion_ops, and they turned out identical. Still, IR and ABI details can vary across compiler versions, so stick to the exact kernel compiler version whenever possible.

Hurdle 3: struct module Missing 16 Bytes

This was the trickiest part of the whole process, and the real culprit behind the “instant reboot upon loading.”

To avoid needing a locally built vmlinux for BTF encoding, I initially disabled these two options:

CONFIG_DEBUG_INFO_BTF
CONFIG_DEBUG_INFO_BTF_MODULES

The module compiled fine, and ksud insmod made it to the loading stage—whereupon the phone instantly rebooted. /sys/fs/pstore was empty, /proc/last_kmsg didn’t exist, and dmesg retained nothing from the previous boot.

With no logs, I had to look at the ABI. Pulling the device BTF and module BTF for a field-by-field comparison revealed:

struct module   Device: 1088 bytes
Module: 1024 bytes

The device had two extra 8-byte fields:

btf_data_size   offset 840
btf_data offset 848

These are guarded by CONFIG_DEBUG_INFO_BTF_MODULES. Because the module was missing those 16 bytes, the kernel read the module’s .gnu.linkonce.this_module section using its own struct module layout. mod->init ended up pointing to a bogus address; the kernel jumped to an invalid pointer, and the phone rebooted.

The fix isn’t simply turning BTF back on. If you turn it on, the module build tries to generate BTF, which in turn requires compiling a full local vmlinux with BTF—a massive undertaking.

The actual trick is to keep the macros enabled during C compilation while skipping the BTF encoding step in make:

make ARCH=arm64 LLVM=1 -j8 M="$MODDIR" \
CONFIG_DEBUG_INFO_BTF= CONFIG_DEBUG_INFO_BTF_MODULES= modules

This way, both macros remain defined in include/generated/autoconf.h, so the C code sees the exact right struct module layout, while passing empty variables on the make command line skips the BTF generation step.

Check the section size in the compiled module:

llvm-readelf -S tcp_bbr.ko | grep this_module
# Expected size 0x440 (1088)

This is quite counterintuitive: for the module to load, you must not disable the BTF config options. The config header dictates struct layout, whereas the build rules dictate whether BTF data is actually generated—the two have to be handled separately.

Other Build Pitfalls

Aside from the three major hurdles above, I ran into a few smaller snags during the build.

TRIM_UNUSED_KSYMS

The device config contains:

CONFIG_TRIM_UNUSED_KSYMS=y
CONFIG_UNUSED_KSYMS_WHITELIST="abi_symbollist.raw"

The source tree doesn’t have abi_symbollist.raw, causing make prepare to fail immediately with:

ERROR: '.../abi_symbollist.raw' whitelist file not found

The fix is to disable symbol trimming:

scripts/config --disable TRIM_UNUSED_KSYMS
scripts/config --set-str UNUSED_KSYMS_WHITELIST ""

This has no effect on the module’s ABI.

Missing asm-offsets.h

modules_prepare doesn’t always generate include/generated/asm-offsets.h. Without it, compilation fails with:

clang: error: invalid value '' in 'mstack-protector-guard-offset='

Generate it manually:

make ARCH=arm64 LLVM=1 prepare
make ARCH=arm64 LLVM=1 arch/arm64/kernel/asm-offsets.s

Missing scripts/module.lds

Linking the module may fail with:

No rule to make target 'scripts/module.lds'

Generate it with:

make ARCH=arm64 LLVM=1 scripts/module.lds

Module Compression

CONFIG_MODULE_COMPRESS_* produces .ko.zst or .ko.xz files, whereas ksud insmod expects a raw .ko. I disabled compression in the build script.

modpost undefined Warnings

Without Module.symvers, modpost will spit out a bunch of warnings like:

WARNING: modpost: "tcp_register_congestion_control" [.../tcp_bbr.ko] undefined!

This is normal. Those symbols will be resolved at load time by ksud insmod via kallsyms and don’t need to be resolved at build time.

Verifying the ABI with BTF

Just because a module loads doesn’t mean its ABI is valid. A much more reliable check is comparing the device BTF against the module BTF.

  1. Pull /sys/kernel/btf/vmlinux from the phone;
  2. Generate BTF from the module’s DWARF info using pahole --btf_encode_detached;
  3. Dump both sides using bpftool btf dump ... format raw;
  4. Compare them field by field.

All key structs must match completely in the end:

Struct Device Module
tcp_sock 2368 2368
tcp_congestion_ops 192 192
rate_sample 72 72
sock 856 856
inet_sock 1088 1088
inet_connection_sock 1536 1536
module 1088 1088

If you see any only-in-device, only-in-module, or MISMATCH, do not load the module. Double-check your source tree, config, and compiler first—never take “it didn’t crash when loaded” as proof that the ABI is right.

scripts/verify-layouts.py in the repository automates this check.

Loading the Module and Setting Up qdisc

Load the module:

su -c 'ksud insmod /data/adb/modules/tcp_bbr_kmod/tcp_bbr.ko'
su -c 'sysctl -w net.ipv4.tcp_congestion_control=bbr'

Confirm that the module registered successfully:

su -c 'cat /proc/sys/net/ipv4/tcp_available_congestion_control'
# reno cubic bbr

The OEM default qdisc is mq + pfifo_fast. While BBR has internal pacing, it works best when paired with fq:

su -c 'tc qdisc replace dev wlan0 root fq'
su -c 'tc qdisc replace dev rmnet_data1 root fq'

On multi-queue devices, setting net.core.default_qdisc=fq won’t override the mq default, so you have to replace the qdisc on each interface individually.

Replacing it doesn’t flatten the hardware queues into a single queue. In Linux 6.1, qdisc_restart() still uses skb_get_tx_queue(dev, skb) to select the hardware queue based on queue_mapping. What changes is the software scheduling layer: multiple TX queues now share a single fq instance, and enqueue/dequeue operations across all queues contend on the same qdisc lock.

On a phone, this trade-off is completely fine. Wi-Fi and cellular speeds are nowhere near high enough for a single qdisc lock to become a bottleneck, whereas fq’s pacing and per-flow queuing are much more beneficial to BBR.

Finally, I packaged everything into a KernelSU module:

  • post-fs-data.sh: loads the .ko as early as possible;
  • service.sh: sets bbr and fq;
  • Background watchdog: checks the qdisc every 30 seconds to prevent netd from resetting fq back to pfifo_fast during network switches.

Verified across reboots: the default CC is bbr, relevant interfaces use fq as their qdisc, and the watchdog runs cleanly under init.

Real-World Performance of This Setup

BBR primarily improves queuing latency under load rather than consistently boosting peak throughput. The noticeable difference in daily use comes from responsiveness, not speedtest numbers.

If your goal is “faster downloads,” changing congestion control on the phone usually yields very little benefit because the download direction is governed by the server. BBR’s impact is much more direct when the phone is the sender—such as uploads, proxy tunnels, and mobile hotspot sharing.

Methodology

From the afternoon into the evening of 2026-11-02, I ran A/B comparisons over Zhejiang Unicom 5G, followed by an additional evening peak session from 22:42 to 22:56. The device was the Redmi K80 on kernel 6.1.138 using tcp_bbr.ko (BBRv1) from the repo. Testing took place indoors on campus, with RSRP hovering just above -100 dBm and SINR ranging from -1 to 14 across different rounds.

There were two test arms:

Arm Congestion Control qdisc
A cubic mq + pfifo_fast (OEM default)
C bbr fq

Only the phone side was changed; the servers remained untouched. Three speedtest servers were used: netcup Manassas (~290 ms), Alibaba Cloud Japan (~110 ms), and Alibaba Cloud Beijing (default config, untuned, ~70 ms). The server software was iliyian/minimal-speedtest. Each round spun up a fresh curl connection in an alternating ABBA pattern with a 3-second pause between rounds; downloads transferred 32 MB and uploads 8 MB. During testing, the module’s 30-second qdisc watchdog was paused, and each round verified the active qdisc via tc and confirmed via ss that the socket actually used the intended CC.

For downloads, the client only sees received bytes—cwnd and delivery_rate in ss are sender-side metrics and remain static on the phone. Therefore, download evaluation looked only at application-layer throughput, loaded ping, and NIC byte counters.

Downloads

None of the three servers showed a reproducible difference in download speeds:

Server CUBIC BBR Paired Median Diff BBR Wins
netcup Manassas 31.50 Mbps 30.45 Mbps -0.7% 3/6
Alibaba Cloud Japan ~42.2 Mbps ~42.2 Mbps Geometric +2.1% 4/5
Alibaba Cloud Beijing (Normal Signal) 24.59 Mbps 24.59 Mbps -17.6% 2/6
Alibaba Cloud Beijing (Evening Peak) 20.45 Mbps 22.75 Mbps +2.2% 3/6

In the Beijing normal-signal row, the medians of both arms were identical, though the BBR arm had two rounds dip to 8.6 / 10.5 Mbps, with pair-wise differences swinging from -57% to +29% and showing no consistent direction. In the evening peak row, the arm medians differed by just 2.2%, with pair-wise swings from -54% to +236%—again, no consistent trend. The Japan figures exclude one round with a sudden signal drop and another interrupted by an incoming phone call.

Beijing also saw one weak-signal session (SINR 0–6) where the BBR arm’s median download was 26% higher, but pair-wise differences ranged from -52% to +102%. Under those conditions, results are dominated by radio-layer retransmissions and scheduling rather than BBR.

The reason is simple: during downloads, the phone is the receiver, and congestion control lives on the server. Changing the CC on the phone doesn’t change how the server sends packets; it only affects the ACK path. To actually get BBR benefits on downloads, BBR has to be enabled on the server.

Uploads

The upload direction was much more consistent. Here the phone is the sender, so its CC is actively in charge. BBR didn’t win on throughput, but its send queues were dramatically shorter:

Server CUBIC BBR Sender RTT cwnd
netcup Manassas 4.97 Mbps 3.52 Mbps 866 → 670 ms 1309 → 376 segs
Alibaba Cloud Japan 6.92 Mbps 6.97 Mbps 443–683 → 190–214 ms 923–1046 → 135–206 segs
Alibaba Cloud Beijing (Normal Signal) 5.46 Mbps 4.40 Mbps 804 → 193 ms 883 → 76 segs
Alibaba Cloud Beijing (Evening Peak) 1.66 Mbps 1.10 Mbps 3983 → 321 ms 809 → 46 segs

The Beijing normal-signal set is the clearest illustration: loaded upload ping p95 dropped from 237 ms to 64 ms, at the cost of ~15% lower throughput. The evening peak set was even more dramatic: with upload speeds constrained to 1–2 Mbps, CUBIC pushed sender RTT to 3.98 s with a 4.09% retransmission rate, whereas BBR kept RTT at 321 ms and retransmissions at 0.99%—trading ~12% throughput for vastly better latency and packet loss. On the Japan server, throughput was basically a tie while BBR shaved 250–480 ms off latency. On netcup, where RTT is high and the path more congested, BBR lost the most throughput, yet still brought RTT down from 866 ms to 670 ms.

This aligns with how BBR works: it paces traffic according to estimated bandwidth instead of filling up buffers, whereas CUBIC grows its window until packet loss occurs, easily bloating deep cellular uplink buffers. On links like this, throughput and latency are a trade-off.

A Packet Loss Incident

During testing, an incoming call rang for 3 seconds before being declined. In that round, BBR’s retransmission rate spiked to 5.32%, transmit overhead hit 7.3%, and throughput dropped from ~7 Mbps to 3.9 Mbps. BBRv1 backs off noticeably when encountering packet loss; under weak signal or link jitter, it won’t necessarily outperform CUBIC.

Conclusion

On this phone, BBR behaves exactly as designed: its job isn’t to inflate peak bandwidth, but to keep queuing delay down.

The most immediate gains are on uploads. Against the default-configured Beijing server, sender RTT dropped from 804 ms to 193 ms and loaded upload ping p95 dropped from 237 ms to 64 ms, while sacrificing only ~15% throughput. During evening peak hours, CUBIC hit 4-second RTTs and 4% retransmissions compared to BBR’s 0.3 seconds and 1%, with only a ~12% throughput difference. Against the Japan server, throughput was essentially tied while shaving 250–480 ms of latency. For video calls, remote desktop, or hotspot sharing, BBR will feel noticeably snappier than CUBIC.

In the download direction, there was neither a reproducible improvement nor a consistent regression. As noted earlier, downloads are dictated by the server’s CC, so enabling BBR on the client shouldn’t be expected to speed them up; server-side configuration is what matters there.

Achieving these latency gains without flashing a custom kernel or wiping the device means this module setup has met its goal. If you care more about peak upload throughput, switching back to CUBIC temporarily is just a single sysctl command away, or you can disable the module to keep CUBIC permanently. If you care about responsiveness and low latency during uploads, BBR is well worth keeping.

These figures apply specifically to BBRv1, this China Unicom link, and these three servers; different networks, servers, or kernel versions may yield different results. Full methodology, round-by-round data, and limitations are available in the repo:

BBRv2 and BBRv3

I also evaluated both newer versions along the way.

BBRv3 cannot be built as a standalone module. It depends on new fields and functions in the core networking stack:

  • struct rate_sample adds tx_in_flight, is_ece, and is_acking_tlp_retrans_seq, which are populated by the core TCP stack;
  • struct tcp_sock adds fast_ack_mode and tlp_orig_data_app_limited;
  • New helper functions like tcp_plb_* and tcp_skb_tx_in_flight_is_suspicious;
  • ICSK_CA_PRIV_SIZE increases from 104 bytes to 144 bytes.

All of these require modifying kernel headers and recompiling the entire kernel, which a loadable module cannot do. Google’s v3 branch is distributed as a full kernel tree for this reason.

2026-11-04 Update: BBRv3 has now been backported to the ACK 6.1.138 kernel and verified on the device.
bbr3 registers and switches cleanly, with boot, audio, and module loading all working normally. For the porting process, core stack patches,
BTF/CRC verification, and troubleshooting notes, see bbrv3/README.md.
The point that “it cannot be built as a module” still stands—this is a full kernel patchset, not a .ko.

BBRv2 is a different story. Compiling tcp_bbr.c from v2alpha-2022-08-28 against ACK 6.1.138 only requires removing the deprecated .tso_segs field; sizeof(struct bbr) is 104 bytes, matching 6.1’s icsk_ca_priv exactly, so BUILD_BUG_ON passes. It only uses fields already present in 6.1 without touching the core stack.

However, v2 is an experimental branch from 2022 that never made it into mainline, and its behavior on mobile networks hasn’t been validated. It would also need to be renamed to bbr2 to coexist with the existing bbr. It’s a viable direction for future tinkering, but I didn’t deploy it this time.

BBRv3 Benchmarks: Latency Gains, No Throughput Gains

Late on 2026-11-04 into the early hours of 11-05, I ran another A/B test on the same K80 with the bbr3 kernel port. This session focused purely on benchmarking without any kernel or patch changes. The network was switched to the default SIM card (China Telecom), which fell back to LTE during testing (download RSRP −87 dBm / SNR 24, upload −94 / SNR 17) rather than the Unicom 5G link from 11-02. The server was still the default-configured Alibaba Cloud Beijing instance.

The test arms matched the previous setup:

Arm Congestion Control qdisc
A cubic mq + pfifo_fast (OEM default)
C bbr3 fq

Each round launched a fresh curl in an alternating ABBA pattern with a 3-second gap between rounds; 6 pairs of 32 MiB downloads and 4 pairs of 8 MiB uploads were run. The bbr3_fq module’s watchdog was paused during testing, and each round verified the qdisc via tc, confirmed the active socket CC via ss, and checked that routing bypassed tun. The unmodified cc_bench.sh harness was used.

Downloads

cubic + mq bbr3 + fq
Median 28.9 Mbps 32.8 Mbps
Mean 28.3 Mbps 32.2 Mbps
Paired Median Diff — +15.8%, 4/6 wins

In pair 4, cubic dipped to 14.8 Mbps (compared to 28–39 Mbps in the other rounds); excluding that outlier reduces the paired median difference to just +2.9% with no consistent direction. Since the phone is the receiver during downloads and congestion control runs on the server, client-side bbr3 only affects ACKs, so any difference cannot be attributed to it.

Uploads

Throughput showed no consistent difference:

Pair cubic bbr3 Diff
1 2.11 Mbps 2.04 Mbps −3.5%
2 2.72 Mbps 3.18 Mbps +17.1%
3 1.60 Mbps 2.10 Mbps +31.3%
4 3.56 Mbps 1.97 Mbps −44.6%

Paired median difference was +6.8%, geometric mean was −4.8%, with 2/4 wins; arm medians were 2.41 vs 2.07 Mbps. There is no reproducible throughput gain.

Queue metrics, on the other hand, showed complete separation:

Metric cubic bbr3
Sender RTT p50 1491 ms 197 ms
Sender RTT p95 2683 ms 388 ms
Sender RTT p99 3084 ms 515 ms
cwnd p50 779 segs 56 segs
cwnd p95 805 segs 69 segs
unacked p50 340 segs 50 segs
Loaded Upload Ping p50 80.5 ms 36.7 ms
Loaded Upload Ping p95 237 ms 135 ms
Samples with RTT > 1 s 48–95% 0% (excl. 1st round)

Looking round by round, cubic’s RTT p95 ranged from 1.9 to 4.8 s, while bbr3 stayed at 0.30–0.42 s (excluding the first round); cwnd p95 was 747–1069 segments vs 57–76 segments (234 in the first round). Cubic’s p95 essentially reflects its buffer ceiling, whereas bbr3’s p95 comes from probe cycles nudging cwnd from ~50 up to ~70.

A sample size of 4 pairs is small. Queue metrics achieved 4/4 complete separation: an exact paired sign-flip test gives p=0.125 (the minimum possible for n=4), and a paired t-test gives p=0.0005–0.011 (assuming normality); paired tests for throughput give p=0.81–0.88. An unpaired permutation test would yield p=0.029 for the queue metrics, but given the paired ABBA design, that figure is overly optimistic and shouldn’t be used to draw conclusions.

Comparison with BBRv1

Recalculating the 11-02 BBRv1 data (Unicom 5G, 12 upload pairs) using the exact same steady-state criteria:

Metric BBRv1 BBRv3
Sender RTT p50 198 ms 197 ms
Sender RTT p95 368 ms 388 ms
Sender RTT p99 419 ms 515 ms
cwnd p95 125 segs 69 segs
Throughput vs cubic Geometric −10.2% Geometric −4.8%
Retransmissions 0.45–3.17% (Evening Peak) 0.28%

There is no distinguishable difference between the two in latency or throughput, though v3 is slightly more conservative with cwnd. However, since this compares data across different days, carriers, and kernels, it shouldn’t be treated as a strict v1-vs-v3 A/B test.

Why There Were No Extra Gains

  • The port used a 2025-03 v3 snapshot with PLB and ecn_low stripped out; those paths would have been dead code on this device anyway, leaving behavior very close to v1.
  • v3’s main improvements over v1 revolve around loss recovery, ECN, and PLB. In this test, total TCP retransmissions on both sides were just 69 segments (0.28%), with zero RTOs, zero SYN retransmits, and no ECN—giving those mechanisms nothing to do.
  • The bottleneck was LTE uplink scheduling at just 2–3.5 Mbps. Both versions kept a cwnd more than large enough to saturate the uplink; any excess cwnd simply turns into queuing delay.
  • On a single flow over a clean link, BBR’s fairness and coexistence improvements don’t come into play either.

Measurement Pitfalls

  • The download direction is structurally invalid for client-side CC testing: the phone is the receiver and CC runs on the server, so nothing changed on the phone can improve download throughput.
  • A 4-pair sample requires paired statistical tests; unpaired permutation artificially lowers p to 0.029, whereas an ABBA design must be analyzed in pairs.
  • ping’s icmp_seq starts at 1. Assuming it starts at 0 fabricates 0.5–2.9% phantom packet loss; actual ICMP loss in this session was 0.
  • Cubic’s deep queue drain can pollute the immediately following bbr3 round. The first bbr3 round had a p95 of 1546 ms compared to 300–415 ms in the subsequent three rounds; a 3-second cooldown isn’t enough—it’s better to wait for the queue to drain completely or add a few extra seconds.
  • The final snapshot from ss sometimes omits the retrans: field, so retransmission counts should be calculated from /proc/net/snmp deltas before and after each run.
  • Active VPNs or multiple interfaces can trick older scripts into monitoring the wrong NIC; in this run, routing went directly out through rmnet_data5, and the results were only valid once the script resolved that interface properly.

Conclusion

On this link, BBRv3’s benefit lies in queue control and tail latency rather than throughput: upload RTT dropped from 1.5 s to 0.2 s, p95 dropped from 2.7 s to 0.4 s, and cwnd shrank from ~780 segments to ~56 segments, at the cost of flat to slightly lower throughput. Compared to BBRv1, there was no distinguishable extra gain, as v3’s loss recovery and ECN had nothing to trigger them on a link with 0.28% retransmissions.

Answering “v1 vs v3” properly requires compiling official BBRv1 into the exact same kernel for a strict A/B test; validating v3’s loss recovery requires testing under weak signal, handovers, or competing flows. Neither was done in this session.

Open Source Repository

The build scripts, KernelSU module, ABI comparison tools, and full troubleshooting notes are all available at:

https://github.com/iliyian/k80-zorn-bbr

The speedtest server used for benchmarking is in a separate repo: iliyian/minimal-speedtest.

The repository includes:

  • scripts/bench/: benchmarking harness and analysis scripts;
  • One-click build script: fetches ACK, fetches AOSP clang, reads device config, compiles, and packages the module;
  • KernelSU module template: module loading, sysctl, fq setup, and watchdog;
  • verify-layouts.py: compares device BTF against module BTF;
  • cfi-hash-test.sh: compares kCFI hashes across different clang versions;
  • Troubleshooting docs and K80 benchmark reports;
  • Prebuilt .ko and module zip for the K80.

The prebuilt artifacts are strictly for 6.1.138-android14-11-g965475777129-mi. Other devices or kernel versions must recompile from source and re-run BTF verification. Although the 6.1.176 kernel now works with normal display output and module loading, the prebuilt .ko in the repo still targets 6.1.138 only; moving to 6.1.176 requires re-verifying or recompiling against the new kernel.

Basic usage of the build script looks like this:

CONFIG_GZ=$PWD/config.gz ./scripts/build-bbr-module.sh

Export config.gz from the device:

adb exec-out su -c 'cat /proc/config.gz' > config.gz

By default, the script fetches the ACK tag and AOSP clang from TUNA, though you can also point it to local paths.

Caveats

  1. A version string containing -androidXX indicates ACK rather than mainline; use the corresponding ACK tag for your source tree.
  2. Check CONFIG_CC_VERSION_TEXT for the compiler version. If CFI is enabled, use the exact same clang version.
  3. Do not disable CONFIG_DEBUG_INFO_BTF*, as doing so alters the layout of struct module.
  4. Always compare BTF before loading a module—never treat “it didn’t crash” as proof of ABI compatibility.
  5. ksud insmod can bypass symbol versioning and vermagic, but it cannot bypass ABI mismatches or CFI checks.
  6. Replace fq per interface; default_qdisc does not take effect automatically on multi-queue devices.
  7. Disable the module before installing system OTAs. Kernel updates may change the ABI, requiring a fresh build against the new kernel.
  8. Instant rollback: sysctl -w net.ipv4.tcp_congestion_control=cubic; permanent removal: disable or delete the KernelSU module and reboot.

6.1.176: Display Compatibility and Dual BBR

On 2026-11-05, I tracked down the 6.1.176 screen flickering to a specific upstream patch rather than “generic kernels being fundamentally incompatible.”

Root Cause of the Screen Flickering

  • Stable 6.1.141 backported upstream commit 41b4b11da0215 (drm: Add valid clones check). With it, drm_atomic_helper_check_modeset() started requiring possible_clones across all encoders attached to the same CRTC to be mutually inclusive.
  • On zorn, the vendor msm_drm.ko (compiled against 6.1.68) attaches both a DSI encoder and a Virtual encoder (encoder_mask=0x6) to CRTC 203, and their possible_clones masks are not reciprocal. As a result, drm_atomic_check_only() returns -EINVAL, causing SDM to fail and retry on every single frame:
[drm:drm_atomic_helper_check_modeset] crtc203 failed valid clone check for mask 0x6
SDM: drmModeAtomicCommit failed with error 22 (Invalid argument). crtc=203
SDM: COMMIT failed: 11
HwcComposer: presentOrValidateDisplay 0 presentFence:-1
  • Version boundary: 6.1.138, 139, and 140 do not have this check; it was introduced in 6.1.141. My earlier conclusion that “6.1.138 is the ceiling for display compatibility” based on 6.1.145/6.1.176 flickering was an overgeneralization—the real culprit was this single stable patch.
  • After reverting drm_atomic_check_valid_clones() and its call site, display output on 6.1.176 works normally, and COMMIT failed / drmModeAtomicCommit failed errors drop from once per frame to 0. Visual output was confirmed normal in real-world use.

Dual BBR

  • Both BBRv1 (bbr) and BBRv3 (bbr3) are now compiled into 6.1.176 simultaneously. BBRv3 lives in a separate net/ipv4/tcp_bbr3.c and registers as bbr3; BBRv1 is retained alongside it, with min_tso_segs adapted to the new tso_segs(sk, mss_now) callback.
  • Defaults are set to:
CONFIG_DEFAULT_TCP_CONG="bbr3"
CONFIG_DEFAULT_NET_SCH="fq"
  • On-device verification: tcp_available_congestion_control = reno bbr bbr3 cubic, default CC is bbr3, and default_qdisc=fq. Both bbr and bbr3 can be switched on the fly and pass live HTTP 200 traffic tests.
  • Display, audio, module loading (501 modules), and KSU root all work normally, with Failed to insmod=0.

Trade-offs and Next Steps

  • Because BBRv3 modifies struct rate_sample, tcp_skb_cb, and tcp_congestion_ops, genksyms generates new CRCs for ~691 exported symbols, causing mismatches against __versions in stock vendor modules. The current test kernel bypasses mismatches inside check_version(); this is a global relaxation, which should later be tightened into a per-symbol stock CRC allowlist or avoided by hiding the new fields from genksyms via __GENKSYMS__.
  • A ThinLTO build and initial on-device verification are complete: -j8 took 24:44.91 with a cgroup peak memory of 6.38 GB, and vmlinux.symvers matches the LTO_NONE build 100%. Boot, display, audio, module loading, and BBR all work normally, with a single-sample boot time ~1% faster; proper performance A/B testing is still pending. See Appendix A.7 for details.
  • For full details, patches, and build configs, see gki176/README.md and Sections 20–22 of BBRV3_PHONE_JOURNAL.md.

Appendix A: Complete Experiment Log

While the main text focuses on how to use the module and what the benchmarks showed, this appendix consolidates the failed attempts, build metrics, and implementation details from the working journal. The timeline spans 2026-11-01 to 11-05, all on the same zorn device.

A.1 Device and Constraints

  • Device: Redmi K80 (zorn / 24117RK2CC), HyperOS OS3.0.309.0.WOKCNXM / Android 16.
  • Stock kernel: 6.1.138-android14-11-g965475777129-mi, KMI android14-6.1, A/B partitioning, unlocked bootloader.
  • Vendor module version skew: msm_drm.ko in /vendor/lib/modules has vermagic 6.1.68-android14-11-gef5271fe2aef-dirty, while /system_dlkm/lib/modules is on 6.1.138. Both belong to the android14-6.1 KMI, allowing them to load across sublevels; jumping to 6.6 or 6.12 would break compatibility.
  • Root: SukiSU Ultra 4.3.0 LKM patched into init_boot_a; boot_a is stock.
  • This bootloader ignores fastboot boot, requiring fastboot flash boot_a for every test; ramoops is cleared on every reboot, so pstore never retains crash logs.

A.2 Early Attempts: Flashing Generic / Third-Party Kernels Directly

Kernel Result Key Observations
Droidspaces 6.1.176 (nbkernel / non-nbkernel) Boots, constant screen flicker 736 / 757 lines of COMMIT failed: 11, presentFence:-1; flashing the zip directly yields the exact same kernel
ShirkNeko 6.1.124 Daily-driver usable 6.1.124-android14-11, boot_completed=1; recovers after 363 early COMMIT failed lines; WiFi SSID 40, 11ax/1200Mbps/WPA3-SAE; 8 camera devices; fingerprint HAL alive with 4 enrolled prints; audio HAL/devices normal (untested with actual playback); built-in KSU v4.1.3, adb shell runs as uid 2000
ShirkNeko 6.1.145 Boots, audio normal, display unusable 1127 lines of COMMIT failed/presentFence, constant screen flicker
Self-built Shirk 6.1.138 Stuck on Redmi logo Compiled cleanly with AOSP clang r487747c/ThinLTO; one failed hunk in fs/namespace.c, empty pstore; later confirmed that hunk wasn’t the root cause
Google GKI 6.1.124 Display normal, audio broken 442 modules loaded; 54 lines of Failed to insmod ... Permission denied; /dev/snd only has timer; /proc/asound/cards empty; va-macro not registered yet, defer 10 times; boot_completed empty
Google GKI 6.1.134 Downloaded, not boot-tested Kept for reference only
Google GKI 6.1.138 + MODULE_SIG_PROTECT=n Working baseline 501 modules loaded; sound card pineapple-mtp-snd-card registered at 6.536 s; 0 va-macro defer occurrences; display/audio/WiFi/5G/camera/fingerprint/KSU root/BBR all normal; actual audio playback confirmed working

Several candidate sources and prebuilts that were ruled out:

  • Allay-OS: android_kernel_xiaomi_sm8650, 6.1.138, lineage-23.2, zorn/POCO F7 Pro, 14 commits, ~2.0 GB; contains audio/display/vendor modules, but lacks zorn DTS and isn’t a self-contained buildable tree.
  • Xiaomi 6.1.68 OSS: bsp-zorn-v-oss in MiCode/Xiaomi_Kernel_OpenSource, based on Qualcomm LA.VENDOR.14.3.0.r1; arch/arm64/boot/dts/vendor is a broken symlink, and techpack/audio is missing from the public repo; usable only as a reference/donor.
  • Sakura prebuilt: Sakura-Devices/device_xiaomi_zorn-kernel is actually 6.1.118-android14-11, not 6.1.138; useful for DTB/vendor module reference, but not a matching baseline.
  • WildKernels: Generic GKI 6.1.157 etc., incompatible with zorn’s vendor display stack out of the box.
  • KyuharuTE/Droidspaces-Kernel-For-K80: The release Image SHAs match the two flickering builds tested above; the repo README also explicitly notes it hasn’t been tested on hardware.

Takeaway: With generic/third-party kernels, newer version numbers don’t guarantee usability—vendor display stack compatibility and module protection are what make or break a build. That 6.1.138 worked at the time didn’t mean 6.1.145+ was fundamentally impossible, as the later 6.1.176 experiments proved.

A.3 The Real Cause of Audio Hangs: GKI Module Protection

Google GKI 6.1.124’s audio failure wasn’t caused by missing config options or SELinux:

  • dmesg showed 54 lines of Failed to insmod '/system_dlkm/.../xxx.ko': Permission denied;
  • Zero avc: denied { module_load } entries;
  • Still returned EACCES after setenforce 0;
  • Kernel cmdline androidboot.selinux=permissive was forced back to Enforcing by user-build init (CONFIG_SECURITY_SELINUX_BOOTPARAM was disabled);
  • modules_disabled=0, no kernel lockdown, and no sig_enforce.

The culprit lives in kernel/module/main.c on android14-6.1:

if (!mod->sig_ok && gki_is_module_protected_export(kernel_symbol_name(s))) {
pr_err("%s: exports protected symbol %s\n", mod->name, ...);
return -EACCES;
}
  • Google’s certified GKI builds ship with a full KMI protected-export table and CONFIG_MODULE_SIG_PROTECT=y;
  • zorn’s stock modules were compiled against Xiaomi’s 6.1.138 key, so on Google’s 6.1.124 GKI they have sig_ok=false. Because they also export protected symbols, the kernel immediately rejects them with -EACCES;
  • Shirk’s standalone ACK build didn’t generate a protection table (NR_UNPROTECTED_SYMBOLS == 0), allowing those exact same modules to load without issue;
  • The failed module loads altered probe ordering: on certified GKI, pineapple-asoc-snd repeatedly hit va-macro not registered yet, defer and the sound card never materialized; on Shirk’s build, it hit 0 defers and registered the sound card in 6.455 s.

Thus, the working baseline for 6.1.138 is simply gki_defconfig + CONFIG_MODULE_SIG_PROTECT=n, rather than hunting down missing audio config options. This finding in Section 18 corrected the earlier hypothesis from Section 8.5.

A.4 Why BBRv3 Cannot Be a Module

BBRv3 isn’t a drop-in tcp_bbr.c replacement; it relies on new struct fields and hooks in the core TCP stack that a loadable module cannot touch:

Dependency Purpose
rate_sample.prior_lost / tx_in_flight / lost / is_acking_tlp_retrans_seq / is_ece Inflight and loss modeling
tcp_skb_cb.tx.in_flight / lost, first_tx_mstamp/delivered_mstamp u64→u32 Per-skb inflight/loss snapshots
ca_ops.skb_marked_lost Callback notifying the CA when the core marks an skb lost
CA_EVENT_TLP_RECOVERY Notification when a TLP probe recovers a loss
ca_ops.tso_segs(sk, mss_now) BBRv3 TSO/GSO sizing budget
fast_ack_mode, tlp_orig_data_app_limited Receiver fast ACK and TLP app-limited snapshot

Attempting to compile the official v3 tcp_bbr.c in the standalone module environment immediately fails with errors like:

use of undeclared identifier 'CA_EVENT_TLP_RECOVERY'
no member named 'lost' in 'struct rate_sample'
no member named 'tx_in_flight' in 'struct rate_sample'
no member named 'is_acking_tlp_retrans_seq' in 'struct rate_sample'

While ksud can work around unexported symbols, it cannot add missing struct fields or make the core kernel populate and invoke them.

Ultimately, the core patch touched 9 files: include/linux/tcp.h, include/net/tcp.h, include/uapi/linux/inet_diag.h, net/ipv4/tcp.c, tcp_cong.c, tcp_input.c, tcp_output.c, tcp_rate.c, and tcp_timer.c. Adapting the congestion control algorithm file itself involved four changes:

  1. Stripping __bpf_kfunc annotations and BTF kfunc registration;
  2. Stripping PLB and ecn_low paths (this kernel lacks RTAX_FEATURE_ECN_LOW, making them dead code);
  3. Adapting cong_control to 6.1’s (sk, rs) signature;
  4. Moving BBR state from inline storage to dynamic allocation and registering under the name bbr3.

Why dynamic state allocation? Even after stripping PLB, struct bbr is still 144 bytes, whereas 6.1’s ICSK_CA_PRIV_SIZE is only 104 bytes. Enlarging icsk_ca_priv would shift every subsequent field in tcp_sock by 40 bytes, breaking ABI compatibility for all vendor networking modules. Instead, icsk_ca_priv stores a compact {state, owner, magic} pointer header while the actual state is allocated via kmalloc and freed in .release, with owner preventing accidental sharing across accept() clones.

Static verification:

  • BTF comparison: tcp_sock stays at 2368 bytes with all existing field offsets unchanged; inet_connection_sock stays at 1536 bytes; rate_sample grows 72→88, tcp_congestion_ops stays 192→192, and tcp_skb_cb shrinks 48→44 (the tx sub-struct size is unchanged, while union alignment drops from 8 to 4).
  • QEMU aarch64 virt: a core-only kernel booted to VFS: Unable to mount root fs in 4.3 seconds with zero panics or BUGs.

The biggest trap was collateral damage from genksyms CRC changes:

  • Touching the TCP headers caused genksyms to compute new CRCs for 691 exported symbols—including functions like skb_put and __alloc_skb whose actual ABIs hadn’t changed at all;
  • audio_pkt_dlkm.ko’s __versions section expected skb_put=0xac7a3881, whereas the new kernel generated 0x385a34f9;
  • When check_version() returns 0 on a mismatch, the module loader fails with -EINVAL. Audio modules fail to load → AudioService/HAL hangs → the phone enters a bootloop;
  • You cannot simply turn off CONFIG_MODVERSIONS: doing so drops the modversions token from vermagic, and since same_magic() compares everything after the first space, every vendor module gets rejected for a vermagic mismatch;
  • The working workaround keeps MODVERSIONS=y and changes the bad_version path in kernel/module/version.c to return 1 (logging via pr_warn_once and allowing the load). This is a pragmatic compromise for a test kernel, not a security hardening measure; proper follow-ups would be a per-symbol stock CRC allowlist or wrapping the new struct fields in #ifndef __GENKSYMS__.

Known limitations: the old standalone tcp_bbr.ko (BBRv1) cannot be loaded on this patched kernel; ECN and PLB are stripped; and this port is based on the 2025-03 BBRv3 snapshot rather than the newer 2026-09 revision (which adjusts DRAIN gain, undo state, and cwnd_event_tx_start).

A.5 Root Cause of the valid-clones Issue on 6.1.176

A pure ACK 6.1.176 build booted fine: boot_completed=1, 501 modules loaded, Failed to insmod=0, working sound card, and working KSU root. However, the screen flickered relentlessly.

Logs showed:

SDM: DRMAtomicReq::Commit: drmModeAtomicCommit failed with error 22 (Invalid argument). crtc=203
SDM: DisplayBase::PerformCommit: COMMIT failed: 11
HwcComposer: presentOrValidateDisplay 0 presentFence:-1

Enabling DRM debug logging revealed the smoking gun:

[drm:drm_atomic_helper_check_modeset] crtc203 failed valid clone check for mask 0x6

Pinpointing the return value via kretprobe:

katomic_check: (drm_atomic_nonblocking_commit+0x1c/0x90 <- drm_atomic_check_only) arg1=0xffffffea
kioctl: (drm_ioctl_kernel+0xf8/0x184 <- drm_mode_atomic_ioctl) arg1=0xffffffea

0xffffffea is -22 (-EINVAL), confirming the failure originates inside drm_atomic_check_only().

Root cause:

  • Stable 6.1.141 backported upstream commit 41b4b11da02157c7474caf41d56baae0e941d01a (drm: Add valid clones check);
  • drm_atomic_check_valid_clones() requires possible_clones across all encoders attached to the same CRTC to be mutually inclusive;
  • zorn’s vendor msm_drm.ko (built on 6.1.68) attaches both DSI and Virtual encoders (encoder_mask=0x6) to CRTC 203 without reciprocal possible_clones masks;
  • Consequently, every modeset involving both encoders fails with -EINVAL, causing HWC to retry on every frame and making the screen flicker.

Version boundary: inspecting drivers/gpu/drm/drm_atomic_helper.c across stable 6.1.138–6.1.176 confirms that 6.1.138, 139, and 140 do not contain this function; it arrived in 6.1.141. Thus, the flickering on 6.1.145 (Shirk) and 6.1.176 (Droidspaces / pure ACK) shared the exact same root cause, disproving the earlier theory that “6.1.138 is the hard ceiling for display compatibility.”

Fix: remove drm_atomic_check_valid_clones() and its call site, restoring 6.1.140 behavior. With Image 841b819e... and boot d34718c8... flashed, COMMIT failed and drmModeAtomicCommit failed dropped from once per frame to 0. While 163 presentFence:-1 lines still appeared, the known-good 6.1.138 baseline exhibits similar bursts (241 lines recorded in §18.5 with completely normal visual output), so that log line alone is not a failure indicator.

A.6 Dual BBR Implementation and Verification

Building on top of the 6.1.176 + valid-clones revert baseline:

  • BBRv1: retains stock net/ipv4/tcp_bbr.c, registering as bbr;
  • BBRv3: adds net/ipv4/tcp_bbr3.c (copied from the adapted 6.1.138 v3 port), registering as bbr3;
  • Core patch 0001-tcp-core-bbrv3.patch applied cleanly to 6.1.176 with 0 rejects, needing only minor line offsets and 1 hunk at fuzz 1;
  • The fuzz 1 hunk was at tcp_process_tlp_ack(sk, ack, flag, &rs), where 6.1.176 added a nearby tcp_in_ack_event(sk, flag) call; the resulting code is completely valid;
  • BBRv1 needed a small adaptation for the new tso_segs(sk, mss_now) callback: added bbr_tso_segs() replicating tcp_tso_autosize(), preserved BBRv1’s min-of-1/2 pacing rate cap, and updated the ops struct member from .min_tso_segs to .tso_segs;
  • Updated Kconfig and Makefile with TCP_CONG_BBR3, DEFAULT_BBR3, default "bbr3" if DEFAULT_BBR3, and obj-$(CONFIG_TCP_CONG_BBR3) += tcp_bbr3.o;
  • Config settings: TCP_CONG_BBR=y, TCP_CONG_BBR3=y, DEFAULT_BBR3=y, DEFAULT_TCP_CONG="bbr3", NET_SCH_DEFAULT=y, DEFAULT_FQ=y, and DEFAULT_NET_SCH="fq";
  • The old tcp_bbr_kmod module remains disabled, while the bbr3_fq KSU module continues to enforce bbr3 + fq during post-fs-data/service and attaches fq to rmnet_data* and wlan*.

On-device verification results:

Item Result
uname -r 6.1.176-android14-11+
tcp_available_congestion_control reno bbr bbr3 cubic
Default CC / qdisc bbr3 / fq
Switching to bbr / back to bbr3 Both succeed
Loaded modules / Failed to insmod 501 / 0
CRC tolerance log 1 line (tolerating symbol version mismatch, matching the 6.1.138 baseline)
Sound card / root / display Normal / KSU LKM / COMMIT failed=0
Live traffic curl https://www.baidu.com returns HTTP 200 under both bbr3 and bbr; ss -tin confirms bbr3 and bbr respectively

A.7 ThinLTO Build and Initial Verification

The stock zorn kernel is built with ThinLTO; this iteration recompiled the exact same 6.1.176 + dual BBR + display fix tree with ThinLTO enabled to verify whether it can replace the LTO_NONE build.

Configuration and toolchain:

  • CONFIG_LTO_CLANG_THIN=y, # CONFIG_LTO_NONE is not set, CONFIG_LTO_CLANG_FULL disabled;
  • CONFIG_CFI_CLANG=y, CONFIG_MODVERSIONS=y, CONFIG_MODULE_SIG_PROTECT=n;
  • Defaults set to bbr3 + fq;
  • Built with Ubuntu clang 17.0.6 (official ACK pin is AOSP r487747c / 17.0.2);
  • Built via make -j8 Image with output in /root/gki176-thinlto.

Build metrics:

Metric Value
Wall time 24:44.91
User / sys 9358.30 s / 1004.97 s
CPU utilization 697%
time -v max RSS 6,124,300 KiB (~5.84 GiB)
cgroup memory.peak 6,381,436,928 B (~5.94 GiB)
memory.swap.peak 1,368,064 B (~1.3 MiB)
memory.events oom=0, oom_kill=0
Image 36,760,064 B, SHA256 59fdfaab...

Compared to LTO_NONE: Image size grew from 35,645,952 B to 36,760,064 B (+3.1%). Exact time and peak memory metrics weren’t recorded for the LTO_NONE run, so build times cannot be compared directly.

ABI check: a full diff (stripped of comments and sorted) between /root/gki176-noprot/vmlinux.symvers and /root/gki176-thinlto/vmlinux.symvers produced 0 lines of difference; module_layout=0xea759d7f, skb_put=0x385a34f9, and __alloc_skb=0x3a1c9278 are identical across both builds. ThinLTO only affects code generation and does not alter genksyms symbol CRCs.

On-device verification:

Item Result
uname -r 6.1.176-android14-11+
/proc/config.gz CONFIG_LTO_CLANG_THIN=y, CONFIG_CFI_CLANG=y
sys.boot_completed 1
Modules / Failed to insmod 501 / 0
CC / qdisc bbr3 / fq
Sound card / root / display Normal / KSU LKM / COMMIT failed=0

Boot progress (single sample):

Metric LTO_NONE ThinLTO
boot_progress_enable_screen 16022 ms 15871 ms
sf_stop_bootanim 17730 ms 17447 ms

Roughly -0.9% and -1.6% faster respectively, though with a single sample and varying background tasks, this cannot be claimed as a statistically significant improvement. CPU, storage, and UI A/B benchmarks have not been run yet.

A.8 Artifacts and Hashes

Artifact SHA256
6.1.176 LTO_NONE Image 263d14815f0d9f58a2e37dff0b8d67a58baa5ca8046f61f1eeb4b7532bb0c47f
6.1.176 LTO_NONE boot 827f8865666e709059230b76c640a1cc35cdea10d49f7253faf9935499440475
6.1.176 ThinLTO Image 59fdfaabfb4bb94239a37b2d1b851be3bcb6bc75a105d83d8296528c34514d559
6.1.176 ThinLTO boot 6637585575048818a2276dd68796ed09094536e013bf2ebcc122517d8221a02b
6.1.176 Display Fix boot d34718c8fdade3deb08d94319024f451847703a34c1878ca155fd3015bcd609e
6.1.138 BBRv3 Image 43b72d3d06717e1df376abf1331d1ca784323914b4e53b5d49c41643ff9e5934
6.1.138 BBRv3 boot 8e6bacabedc9e347cc41a68a77dc9b630bbd6b6dffcb000adc8aa82784e28354
stock boot_a a1a041eb90226266244844c45adf650f665e930f622cc1db56afb95aba38affd

GitHub release: https://github.com/iliyian/k80-zorn-bbr/releases/tag/v0.2.0-6.1.176

A.9 Open Items and Risks

  • check_version() currently relaxes CRC checks globally; this should be narrowed down to a per-symbol stock CRC allowlist or avoided by hiding the new fields from genksyms via __GENKSYMS__.
  • CONFIG_MODULE_SIG_PROTECT=n is a deliberate trade-off to allow stock vendor modules to load; MODULE_SIG_FORCE was never enabled to begin with, as MODULE_SIG_PROTECT is Android’s extra gatekeeper.
  • Reverting valid-clones removes a DRM validation check rather than a display feature; it restores 6.1.140 behavior, though strictly speaking it drops a sanity check that upstream intended to enforce.
  • The BBRv3 port strips out ECN and PLB, and the newer 2026-09 snapshot has not been ported yet.
  • WiFi, camera, fingerprint, and actual speaker/headphone playback have not yet undergone systematic regression testing on 6.1.176, nor have long-term stability, thermals, or weak-signal/high-loss scenarios been stress-tested.
  • Performance A/B testing for ThinLTO is still pending; PGO and AutoFDO lack out-of-the-box support on 6.1 ACK and would require additional patchsets and profile collection.
  • The old standalone tcp_bbr.ko cannot be loaded on the new custom kernels; the prebuilt .ko in the repository remains strictly for stock 6.1.138.