Field report / Security

Building Kali NetHunter for the POCO M4 5G Without Guessing

What it took to bring NetHunter to Xiaomi's MediaTek-based POCO M4 5G—from an unsafe first kernel build to working HID and external Wi-Fi.

#android#kali-nethunter#kernel#poco-m4-5g#mediatek#usb-hid#rtl88xxau
Technical recordPOCO-M4-5G-NETHUNTER-BRING-UP
Device
POCO M4 5G (light / 22041219PI)
SoC
MediaTek MT6833, ARM64
ROM
HyperOS Android 14 / U, V816.0.4.0.ULSINXM
Kernel
Linux 4.19.191
Toolchain
Android clang r383902 / LLD 11.0.1
Status
Validated v0.1.0 reference build

I went into this project with a simple goal: turn my POCO M4 5G into a useful Kali NetHunter device. On paper, that sounded like a kernel configuration job enable the right options, compile, boot, done.

It was not that simple.

The first kernel that compiled cleanly was not safe to boot. HID support worked, but not through the device nodes I expected. The external Wi-Fi adapter appeared as wlan2, then crashed the phone in two different CFI paths. Even ADB and HID, which both worked separately, found creative ways to break when I put them into the same USB gadget.

By the end, I had a public v0.1.0 research reference for the Indian POCO M4 5G (light) on HyperOS Android 14: an ABI-clean kernel baseline, working keyboard HID, an accepted ADB + HID composite, USB serial support, and an archived RTL88XXAU build that passed monitor-mode and passive radiotap capture tests.

Some pieces are still unfinished. Mouse HID needs more work, Kali does not yet have native Bluetooth, and I have not reconstructed the exact final RTL88XXAU compatibility changes into a clean public patch. I would rather say that plainly than turn a useful research build into a misleading success story.

If you only want the story, read straight through. If you are here because you have the same phone and want to repeat the work, the practical route is in How to follow this project. It includes the actual source commit, configuration checkpoints, build wrapper, ABI gate, and the points where this public archive deliberately stops.

What changed, in plain English

There are a few different systems involved here, and it is easy to blur them together. This is the short version of what I changed and where it lives:

Layer What changed Why
Xiaomi kernel baseline Rebuilt from the Android-U source/config and checked against the ROM’s module ABI Establish a kernel that could safely host the existing vendor modules
Kernel USB gadget code Enabled ConfigFS functions and fixed Xiaomi’s HID descriptor overwrite Let NetHunter create its keyboard and mouse functions correctly
NetHunter userspace Patched USB Arsenal state handling and resolved HID nodes dynamically Preserve ADB and stop assuming that keyboard always means /dev/hidg0
USB serial Enabled CH341, CP210x, FTDI, and PL2303 drivers Support common serial adapters from Kali
External Wi-Fi Integrated RTL88XXAU and corrected CFI-sensitive driver paths in the archived build Provide monitor mode and passive capture through a USB adapter
Boot animation Added a separate Magisk overlay Change the animation without writing to the read-only product partition

The kernel, Android USB state, NetHunter scripts, and Magisk module are separate layers. A fix in one does not automatically fix the others. Keeping those boundaries clear saved me from rebuilding the kernel for what turned out to be a userspace mapping problem.

Start with the phone that actually exists

Before touching the kernel, I recorded the device as it was running:

Component Verified target
Device POCO M4 5G, light, model 22041219PI
SoC MediaTek MT6833, ARM64
ROM HyperOS Android 14 / U, V816.0.4.0.ULSINXM
Kernel Linux 4.19.191
Compiler family Android clang r383902, clang/LLD 11.0.1

The stock kernel source came from Xiaomi’s official light-u-oss branch at commit a40ceab8943703f33e935cb0b86d0f69aa2044dd. I pinned the commit as well as the branch because branches move. Months later, I wanted to be able to answer “which Xiaomi source did this actually come from?” with something better than a vague repository link.

Xiaomi’s published tree is not a perfect reconstruction of what shipped on the phone. It expects the MIHW tree at vendor/xiaomi/kernel_modules/mihw/, but that source is absent. Disabling the unavailable pieces got the build moving again. It also created the first trap: the compiler finished successfully, which felt like progress, but said nothing about whether the phone’s existing vendor modules could safely use that kernel.

The first successful build was not a safe build

Android vendor modules have to agree with the kernel’s exported symbol ABI. This device uses CONFIG_MODVERSIONS, so imported kernel symbols carry CRCs. Those CRCs describe the ABI a module was built against.

My early candidate looked convincing at first. The release string was right, the module vermagic looked right, and there was a fresh kernel image sitting in the output directory. Then I compared every imported module symbol against the kernel’s exported symbol table.

ABI audit stage Kernel matches CRC mismatches External or unlisted imports
Early candidate 986 8 79
Correct HyperOS U baseline 994 0 79

Eight CRCs did not match. The list included core symbols such as wake_up_process and set_user_nice, plus vendor-facing connectivity and MMC exports. I quarantined that build instead of booting it “just to see.” At that point, a failed boot would not have taught me anything I did not already know: the kernel and its modules disagreed about their ABI.

After moving to the correct Android-U baseline, the same audit found 994 matching kernel imports and zero CRC mismatches. The other 79 imports were external or absent from the kernel’s own symbol table, so I kept them in a separate bucket instead of quietly counting them as successes.

Build in layers, not in one heroic patch

With a baseline I could trust, I resisted the urge to throw every NetHunter option into one giant build. I split the work into four lineages:

  1. an ABI-clean baseline;
  2. USB gadget and HID support;
  3. USB serial support;
  4. an external Wi-Fi reference build.

I saved the configuration and Module.symvers at each stage. It was a little extra housekeeping that paid for itself repeatedly. When something broke, I could compare it with the last known-good layer instead of staring at one huge patch and guessing.

The test repacks kept the rooted stock ramdisk and stock DTB. That constrained the investigation to the intended kernel changes and avoided quietly mixing device-tree or userspace experiments into the same result.

HID worked only after following ConfigFS all the way down

HID looked like it should be easier. The kernel had the gadget functions, the phone had USB-C, and NetHunter already knew how to drive a keyboard gadget. In practice, Xiaomi’s Android userspace and the live ConfigFS state had the final say in what the connected computer actually saw.

The first problem was Xiaomi’s f_hid behavior: the report descriptor could be overwritten instead of preserved. Fixing that made the function usable, but my test payloads still went nowhere when I wrote to the device nodes I expected.

The active gadget was g1, bound to the musb-hdrc UDC. The clue was that a ConfigFS function name did not reliably predict its character-device number. For example, hid.0 and hid.1 could appear as /dev/hidg1 and /dev/hidg2, not the tempting /dev/hidg0 and /dev/hidg1 pair.

Once I stopped assuming and read the live mapping, the path became clear: discover the node, verify the report length, and use what the running gadget actually created.

The payoff was wonderfully ordinary: the phone typed a harmless hello world on the host. Keyboard HID was real. Mouse HID is still only partially working, so it stays on the unfinished list.

USB Arsenal had to cooperate with Android

That small keyboard win immediately led to the next problem. A standalone HID function was one thing; asking NetHunter’s USB Arsenal to keep ADB alive beside it was another. Android already manages USB through properties, init services, FunctionFS, and an ADB daemon that becomes ready on its own schedule.

Several failures came from stale gadget state rather than missing kernel support:

  • old ORI_FUNCS state was reused;
  • stale ConfigFS links survived a transition;
  • ffs.adb could be linked twice;
  • Android init raced with FunctionFS and the application;
  • a composite could look correct in ConfigFS while ADB was not yet ready.

I eventually stopped treating each attempt as a clean slate, because it was not. The working approach inspected the live gadget, removed only stale links, avoided duplicate FunctionFS functions, and respected Android’s property-driven ADB lifecycle. The accepted result was the application-button ADB + HID composite. I am not claiming that every boot-time persistence path is solved.

One small but important caveat: the accepted script is in the public record and the composite was functionally tested, but I do not have a recorded, hash-matched functional rerun after reboot. I am not going to retroactively invent one. Good notes should tell the next person what happened, not what would make the neatest timeline.

USB serial support was the straightforward part

After all of that, USB serial was pleasantly boring. I added support for the common adapter families I wanted:

  • CH341;
  • CP210x;
  • FTDI;
  • PL2303.

Then I rebuilt, saved the new config and symbols, and ran the ABI audit again. Even boring changes have to pass the same gate.

External Wi-Fi: enumeration is not stability

The external Wi-Fi target was a TP-Link adapter with USB ID 2357:0120, using the RTL88XXAU driver. Work began from NetHunter’s Linux 4.19 rtl88xxau-5.6.4.2 patch family.

The early result looked great: the adapter enumerated and appeared as wlan2. For a moment, it looked as if the difficult part was over. Then the phone rebooted.

One build hit a CFI failure at rtw_xmit_entry. A later revision got farther, only to fail at usb_recv_tasklet and __cfi_check_fail. Without persistent logs, either crash could have looked like a flaky cable, a power problem, or just another unexplained Android reboot.

pstore/ramoops broke that guessing loop. The crash evidence survived the reboot, pointed to the actual call sites, and showed that USB detection was not the problem. The driver and the kernel’s Control Flow Integrity checks disagreed.

The final archived reference build passed these tests:

  • adapter enumeration as wlan2;
  • monitor-mode transition;
  • passive radiotap capture;
  • repeated short-session use on the tested device.

I am not making a public injection claim, and suspend/resume testing is still limited. There is another uncomfortable detail: I have the working archived build, but I have not yet reconstructed the precise compatibility delta between it and the CFI-crashing revisions as a clean source patch. That makes it a useful, tested reference—not yet a reproducible Wi-Fi package.

Why Android Bluetooth works but Kali has no hci0

Bluetooth produced one of the most confusing results in the whole project: it worked perfectly well in Android, while Kali could not see an hci0 device at all. Both observations were correct.

The stock Android stack talks to the MediaTek connectivity subsystem through the vendor CONNAC/WMT path. The device evidence includes the MT6631 family, bt_drv_connac1x, wmt_drv, 1100c000.btif, /dev/stpbt, and MediaTek’s Bluetooth HAL.

The shipping kernel, however, does not expose the normal Linux Bluetooth stack expected by BlueZ: CONFIG_BT and RFKill support are absent, and there is no native hci0 or AF_BLUETOOTH path for Kali.

Flipping CONFIG_BT would not magically turn MediaTek’s vendor transport into a standard Linux HCI device. That needs actual integration across the kernel, vendor driver, and userspace. For now, native Kali Bluetooth is not implemented, and Android Bluetooth is left alone and working.

Systemless finishing work

Not every finishing touch needed a kernel rebuild. I wanted the NetHunter boot animation too, but /product is read-only on this dynamic-partition layout. A small Magisk module overlaid the relevant paths without touching the real partition. After correcting the frames to the phone’s 1080×2408 display, it survived a normal reboot.

It was the right tool for the job: reversible, systemless, and nowhere near a critical partition for what was ultimately a cosmetic change.

How to follow this project

This is the route I would use if I were starting again. It is a build and validation guide, not a flashing recipe. Android boot images contain device-specific and often proprietary pieces, so the public project does not ship one and this article does not tell you to flash an unverified image.

1. Confirm that you actually have the same target

Do not begin with the box label or a marketplace listing. Record what the running phone reports:

adb shell getprop ro.product.device
adb shell getprop ro.product.model
adb shell getprop ro.build.fingerprint
adb shell uname -a
adb shell 'zcat /proc/config.gz 2>/dev/null | grep CONFIG_MODVERSIONS'

For this project, the important anchors are light, model 22041219PI, HyperOS Android U build V816.0.4.0.ULSINXM, and Linux 4.19.191. Stop if the codename, ROM generation, kernel family, or module set differs. The method may still be useful, but the saved configs and ABI result no longer prove anything about your phone.

Before any boot testing, know the active slot, confirm that you can reach the bootloader independently of Android, and keep verified stock and rooted boot backups somewhere other than the phone. Record their SHA-256 values. The project’s recovery checklist is required reading, not an appendix for after something breaks.

2. Check out the two pinned source trees

Use the frozen public release for the project record and the exact Xiaomi commit used by the build:

git clone https://github.com/N3tm4t3/poco-m4-5g-nethunter.git
git -C poco-m4-5g-nethunter checkout v0.1.0

git clone https://github.com/MiCode/Xiaomi_Kernel_OpenSource.git xiaomi-light-kernel
git -C xiaomi-light-kernel checkout a40ceab8943703f33e935cb0b86d0f69aa2044dd

The project repository is small because it does not vendor Xiaomi’s full tree, the Android toolchain, proprietary modules, firmware, or boot images.

3. Recreate the compiler environment

The accepted build used Android clang r383902 with clang/LLD 11.0.1, plus GNU AArch64 and ARM compatibility cross-compilers. You will also need the usual Linux kernel build dependencies: GNU make, bc, bison, flex, Python, Git, and the OpenSSL and ELF development headers.

The exact clang bundle is not included. Obtain it from a source you are allowed to use and verify the identity instead of substituting whatever clang happens to be installed globally:

/absolute/path/to/clang-r383902/bin/clang --version
/absolute/path/to/clang-r383902/bin/ld.lld --version
aarch64-linux-gnu-gcc --version
arm-linux-gnueabi-gcc --version

A newer compiler is not automatically a better compiler for reproducing an Android vendor kernel. Changing source, config, and toolchain at the same time makes any later difference much harder to explain.

4. Build the baseline first

Start with configs/baseline/.config, not the Wi-Fi checkpoint. The supplied wrapper insists on absolute paths and writes to a separate output directory:

cd poco-m4-5g-nethunter

export KERNEL_SRC=/absolute/path/to/xiaomi-light-kernel
export TOOLCHAIN=/absolute/path/to/clang-r383902
export OUT=/absolute/path/to/build-output/baseline
export CONFIG="$PWD/configs/baseline/.config"

scripts/build/build-kernel.sh

The wrapper copies the selected config, runs olddefconfig, then builds the kernel image, modules, and DTBs. It prints hashes for the resulting Image, effective .config, and Module.symvers.

Do not ignore configuration drift. Compare what went in with what olddefconfig produced:

diff -u "$CONFIG" "$OUT/.config"

Some normalization can be expected, but every unexplained change deserves a look. The missing Xiaomi MIHW source is part of the reason a simple stock build is not possible from the published tree.

5. Run the ABI gate before considering a boot

This step is what rejected my first apparently successful kernel. The checker expects the new Module.symvers and a tab-separated import list derived from the vendor modules on your own ROM:

module-name.ko<TAB>symbol_name<TAB>0xCRC

Then run:

scripts/abi-check/check-symbol-crcs.sh \
  "$OUT/Module.symvers" \
  /absolute/path/to/module-imports.tsv

For my retained module set, the accepted baseline reported:

TOTAL_IMPORTS=1073
KERNEL_MATCHES=994
KERNEL_MISMATCHES=0
EXTERNAL_OR_UNLISTED=79

The public repository intentionally does not include Xiaomi’s proprietary .ko files or the extracted import table, and it does not yet provide an end-to-end extractor for that TSV. You must derive it from modules you lawfully hold and audit the result. That is a real reproducibility gap, not a file to download from an unofficial mirror.

If a symbol exists in both datasets with a different CRC, stop. Do not explain it away with matching uname, vermagic, or a successful compile.

6. Add one feature layer at a time

The repository preserves four checkpoints:

Checkpoint Purpose Extra work
configs/baseline Stock-compatible Android-U baseline Establish this first
configs/usb-v1 ConfigFS gadget functions and HID Apply the reviewed f_hid patch only if its context matches
configs/usb-v2 Focused USB serial drivers Rebuild and repeat the ABI audit
configs/wifi-stable Archived Wi-Fi configuration Requires the cited NetHunter RTL driver and a final compatibility delta not fully published here

For each layer: use a new output directory, preserve .config and Module.symvers, rerun the symbol check, and test the baseline phone functions before testing the new feature. That ordering makes it much easier to tell whether a failure came from the kernel, Android userspace, or the new device.

The Wi-Fi checkpoint needs special caution. The starting NetHunter driver patch is identified in the RTL88XXAU source note, but the minimal final CFI/compatibility delta was not retained separately. A fresh clone can reproduce the configuration and the earlier failure path; it cannot honestly reproduce the exact accepted Wi-Fi build yet.

7. Treat repacking and device testing as a separate gate

My private test images used Android boot header v2. I replaced only the kernel payload while preserving the already rooted stock/Magisk ramdisk and stock DTB. Every repack was unpacked again and its component hashes and header fields were compared with the intended inputs.

I am intentionally not including a generic flashing command here. Slot layout, bootloader behavior, recovery access, and the correct stock components must be verified on the reader’s actual device. The public archive supplies neither a flashable image nor the proprietary material needed to construct one.

When a candidate does boot, test in this order:

  1. normal Android boot and lock/home screen;
  2. Magisk/root;
  3. internal Android Wi-Fi and Bluetooth;
  4. ADB and charging behavior;
  5. the one newly added kernel feature;
  6. suspend/resume where relevant;
  7. pstore/ramoops after any reboot or crash.

“The adapter appeared” is not an acceptance test. For the final external Wi-Fi reference I checked driver binding, wlan2, monitor mode, passive radiotap capture, ordinary Android connectivity, runtime behavior, and post-crash evidence. I did not turn that into an injection claim.

8. Apply userspace patches only after reviewing their anchors

The USB Arsenal and DuckHunter changes are not generic installers. They were written around the ConfigFS gadget g1, the musb-hdrc UDC, Android’s FunctionFS ADB lifecycle, and NetHunter 2026.2-era scripts.

Before applying anything, compare the patch with the exact script installed on your device. The USB Arsenal provenance record documents every retained script hash and distinguishes the authoritative final application-button-tested version from tracing and rejected persistence stages. The read-only resolve-hid-node.sh shows the safer pattern: derive the character device from ConfigFS instead of assuming /dev/hidg0.

What v0.1.0 actually means

The public release freezes the following state:

Area v0.1.0 status
Kernel baseline ABI-clean for the documented HyperOS U target
Keyboard HID Working and harmlessly validated
Mouse HID Partial; needs more testing
USB Arsenal Application-button ADB + HID composite accepted
USB serial CH341, CP210x, FTDI, and PL2303 included
External RTL88XXAU Wi-Fi Archived reference build passed monitor mode and passive capture
Wi-Fi reproducibility Exact final CFI/compatibility delta still to reconstruct
Native Kali Bluetooth Not implemented; Android vendor path remains functional
Internal MediaTek Wi-Fi monitor mode Not established

It is not the flashiest status table, but it is one I can defend.

Where the project goes next

There is plenty left to explore when I return to the project:

  1. reconstruct the exact final RTL88XXAU CFI/compatibility delta;
  2. finish HID mouse validation;
  3. expand suspend/resume and long-session testing;
  4. investigate a genuine MediaTek transport path to native hci0 and BlueZ;
  5. determine whether internal MediaTek Wi-Fi monitor mode is realistic;
  6. package the patch and build process more reproducibly.

That work belongs in a future release. I am leaving the v0.1.0 tag where it is: a snapshot of the initial public research, not a pointer that moves every time I learn something new.

The takeaway

Looking back, the most valuable result is not the boot logo or even the list of working features. It is the process I ended up using:

  • identify the exact shipping target;
  • pin the real upstream source and toolchain;
  • compare symbol ABIs before booting;
  • introduce one feature family at a time;
  • keep known-good rollback points;
  • capture persistent crash evidence;
  • separate enumeration from functional validation;
  • document negative results and provenance boundaries.

That process turned an unsupported MediaTek phone into a genuinely useful NetHunter research platform. It also stopped me from confusing “it compiled,” “it appeared once,” and “it is proven to work”—three very different milestones.

The complete source history and evidence are public in the poco-m4-5g-nethunter repository. The deeper procedures and architecture notes live in the project documentation, and the frozen initial research state is documented in the v0.1.0 release.

End of field report

Last updated Aug 10, 2026#android · #kali-nethunter · #kernel · #poco-m4-5g · #mediatek · #usb-hid · #rtl88xxau