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 |
Next, check the kernel configuration:
$ zcat /proc/config.gz | grep TCP_CONG |
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_clonesin zorn’s vendormsm_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:
- vermagic: The kernel version string embedded in the module at build time must match the running kernel;
- MODVERSIONS: If the kernel has
CONFIG_MODVERSIONSenabled, every external symbol referenced by the module carries a CRC checksum that the kernel verifies one by one; - 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:
- Reads the
.koand parses undefined symbols in the ELF; - Resolves the address of each symbol via the kernel symbol query interface (UAPI 6) or
/proc/kallsyms; - Rewrites the symbol table entries to
SHN_ABSwith their values set to the real addresses; - Calls
init_module; - 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.138is the upstream stable version;-android14-11indicates theandroid14-6.1branch of the Android Common Kernel (ACK);-g965475777129is the commit hash of the build tree;-mirepresents Xiaomi’s own patches.
ACK is not mainline. It includes Android-specific patches and adds extra fields to kernel structs:
android_kabi_reserved*, controlled byCONFIG_ANDROID_KABI_RESERVE;android_oem_data1, controlled byCONFIG_ANDROID_VENDOR_OEM_DATA;- Backported ACK fields like
sk_wait_pending; btf_data_size/btf_datainsidestruct 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 |
Verify the version:
head -5 Makefile |
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, |
And the kernel also enables:
CONFIG_CFI_CLANG=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 \ |
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 |
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 |
The device had two extra 8-byte fields:
btf_data_size offset 840 |
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" \ |
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 |
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 |
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 |
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 |
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.
- Pull
/sys/kernel/btf/vmlinuxfrom the phone; - Generate BTF from the module’s DWARF info using
pahole --btf_encode_detached; - Dump both sides using
bpftool btf dump ... format raw; - 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' |
Confirm that the module registered successfully:
su -c 'cat /proc/sys/net/ipv4/tcp_available_congestion_control' |
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' |
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.koas early as possible;service.sh: setsbbrandfq;- Background watchdog: checks the qdisc every 30 seconds to prevent
netdfrom resettingfqback topfifo_fastduring 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:
- Report: docs/bench/2026-11-02-bbr-vs-cubic.md
- Per-round CSV: docs/bench/data/2026-11-02-runs.csv
- Per-sample data: ss / ping
- Repository: https://github.com/iliyian/k80-zorn-bbr
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_sampleaddstx_in_flight,is_ece, andis_acking_tlp_retrans_seq, which are populated by the core TCP stack;struct tcp_sockaddsfast_ack_modeandtlp_orig_data_app_limited;- New helper functions like
tcp_plb_*andtcp_skb_tx_in_flight_is_suspicious; ICSK_CA_PRIV_SIZEincreases 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_lowstripped 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
cwndmore than large enough to saturate the uplink; any excesscwndsimply 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’sicmp_seqstarts 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
sssometimes omits theretrans:field, so retransmission counts should be calculated from/proc/net/snmpdeltas 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
.koand 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
- A version string containing
-androidXXindicates ACK rather than mainline; use the corresponding ACK tag for your source tree. - Check
CONFIG_CC_VERSION_TEXTfor the compiler version. If CFI is enabled, use the exact same clang version. - Do not disable
CONFIG_DEBUG_INFO_BTF*, as doing so alters the layout ofstruct module. - Always compare BTF before loading a module—never treat “it didn’t crash” as proof of ABI compatibility.
ksud insmodcan bypass symbol versioning and vermagic, but it cannot bypass ABI mismatches or CFI checks.- Replace
fqper interface;default_qdiscdoes not take effect automatically on multi-queue devices. - Disable the module before installing system OTAs. Kernel updates may change the ABI, requiring a fresh build against the new kernel.
- 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 requiringpossible_clonesacross 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 theirpossible_clonesmasks 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 |
- 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, andCOMMIT failed/drmModeAtomicCommit failederrors 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 separatenet/ipv4/tcp_bbr3.cand registers asbbr3; BBRv1 is retained alongside it, withmin_tso_segsadapted to the newtso_segs(sk, mss_now)callback. - Defaults are set to:
CONFIG_DEFAULT_TCP_CONG="bbr3" |
- On-device verification:
tcp_available_congestion_control = reno bbr bbr3 cubic, default CC isbbr3, anddefault_qdisc=fq. Bothbbrandbbr3can 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, andtcp_congestion_ops,genksymsgenerates new CRCs for ~691 exported symbols, causing mismatches against__versionsin stock vendor modules. The current test kernel bypasses mismatches insidecheck_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 fromgenksymsvia__GENKSYMS__. - A ThinLTO build and initial on-device verification are complete:
-j8took 24:44.91 with a cgroup peak memory of 6.38 GB, andvmlinux.symversmatches theLTO_NONEbuild 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.mdand Sections 20–22 ofBBRV3_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), HyperOSOS3.0.309.0.WOKCNXM/ Android 16. - Stock kernel:
6.1.138-android14-11-g965475777129-mi, KMIandroid14-6.1, A/B partitioning, unlocked bootloader. - Vendor module version skew:
msm_drm.koin/vendor/lib/moduleshas vermagic6.1.68-android14-11-gef5271fe2aef-dirty, while/system_dlkm/lib/modulesis on 6.1.138. Both belong to theandroid14-6.1KMI, 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_ais stock. - This bootloader ignores
fastboot boot, requiringfastboot flash boot_afor every test;ramoopsis cleared on every reboot, sopstorenever 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-ossinMiCode/Xiaomi_Kernel_OpenSource, based on QualcommLA.VENDOR.14.3.0.r1;arch/arm64/boot/dts/vendoris a broken symlink, andtechpack/audiois missing from the public repo; usable only as a reference/donor. - Sakura prebuilt:
Sakura-Devices/device_xiaomi_zorn-kernelis actually6.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
ImageSHAs 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:
dmesgshowed 54 lines ofFailed to insmod '/system_dlkm/.../xxx.ko': Permission denied;- Zero
avc: denied { module_load }entries; - Still returned
EACCESaftersetenforce 0; - Kernel cmdline
androidboot.selinux=permissivewas forced back to Enforcing by user-buildinit(CONFIG_SECURITY_SELINUX_BOOTPARAMwas disabled); modules_disabled=0, no kernel lockdown, and nosig_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))) { |
- 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-sndrepeatedly hitva-macro not registered yet, deferand 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' |
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:
- Stripping
__bpf_kfuncannotations and BTF kfunc registration; - Stripping PLB and
ecn_lowpaths (this kernel lacksRTAX_FEATURE_ECN_LOW, making them dead code); - Adapting
cong_controlto 6.1’s(sk, rs)signature; - 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_sockstays at 2368 bytes with all existing field offsets unchanged;inet_connection_sockstays at 1536 bytes;rate_samplegrows 72→88,tcp_congestion_opsstays 192→192, andtcp_skb_cbshrinks 48→44 (thetxsub-struct size is unchanged, while union alignment drops from 8 to 4). - QEMU aarch64
virt: a core-only kernel booted toVFS: Unable to mount root fsin 4.3 seconds with zero panics or BUGs.
The biggest trap was collateral damage from genksyms CRC changes:
- Touching the TCP headers caused
genksymsto compute new CRCs for 691 exported symbols—including functions likeskb_putand__alloc_skbwhose actual ABIs hadn’t changed at all; audio_pkt_dlkm.ko’s__versionssection expectedskb_put=0xac7a3881, whereas the new kernel generated0x385a34f9;- 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 themodversionstoken from vermagic, and sincesame_magic()compares everything after the first space, every vendor module gets rejected for a vermagic mismatch; - The working workaround keeps
MODVERSIONS=yand changes thebad_versionpath inkernel/module/version.cto return 1 (logging viapr_warn_onceand 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 |
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 |
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()requirespossible_clonesacross 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 reciprocalpossible_clonesmasks; - 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 asbbr; - BBRv3: adds
net/ipv4/tcp_bbr3.c(copied from the adapted 6.1.138 v3 port), registering asbbr3; - Core patch
0001-tcp-core-bbrv3.patchapplied 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 nearbytcp_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: addedbbr_tso_segs()replicatingtcp_tso_autosize(), preserved BBRv1’s min-of-1/2 pacing rate cap, and updated the ops struct member from.min_tso_segsto.tso_segs; - Updated
KconfigandMakefilewithTCP_CONG_BBR3,DEFAULT_BBR3,default "bbr3" if DEFAULT_BBR3, andobj-$(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, andDEFAULT_NET_SCH="fq"; - The old
tcp_bbr_kmodmodule remains disabled, while thebbr3_fqKSU module continues to enforcebbr3 + fqduringpost-fs-data/serviceand attachesfqtormnet_data*andwlan*.
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_FULLdisabled;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 Imagewith 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 fromgenksymsvia__GENKSYMS__.CONFIG_MODULE_SIG_PROTECT=nis a deliberate trade-off to allow stock vendor modules to load;MODULE_SIG_FORCEwas never enabled to begin with, asMODULE_SIG_PROTECTis Android’s extra gatekeeper.- Reverting
valid-clonesremoves 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.kocannot be loaded on the new custom kernels; the prebuilt.koin the repository remains strictly for stock 6.1.138.
