← Back to blog

How I Turned a Samsung S20 Ultra into a Linux Handheld Without Official Support

17 August 2026 · 6 min read

linuxsecurity

Built hands-on, on a phone with no official support and almost no documentation online.

Why this project exists

An old Samsung Galaxy S20 Ultra was lying around at home without much use. Instead of leaving it in a drawer, I decided to make something genuinely usable out of it: a proper Linux and pentesting handheld, not a weekend toy.

Kali NetHunter's only official reference is the Note20 Ultra (Exynos 990, codename c2s). The Galaxy S20 Ultra runs on a different chip (z3s) and has no official support at all. Looking for what already existed, I found one older community predecessor for exactly this chip. But it dates from 2023, targets Android 13, flashes through TWRP, and by its own description only partly works (Bluetooth with caveats, one tested Wi-Fi adapter, USB Arsenal not working). In discussions I also saw people on the current LineageOS 23/Android 16 branch for this phone looking for a working NetHunter kernel and not finding one.

So not an empty field, but a real gap for the current version, and that's what I ended up working on.

The kernel port

The base is the exact source revision of the installed LineageOS 23.2 build. On top of it I fixed three specific kernel bugs, all of which showed up in real use rather than from reading code:

  • Bluetooth crash on hciconfig: hci_sock_create/bind/release/ioctl/getname were wrapped in #if 0 in the source with a faulty return 0 fallback, so hci_sock_create never called sk_alloc. Without this fix, the kernel crashed on any attempt at Bluetooth diagnostics.
  • Unsupported Bluetooth command: bredr_setup() sent HCI_OP_SET_EVENT_FLT, which my CSR8510 test dongle doesn't support. The kernel treated the "Unknown HCI Command" reply as a fatal error instead of ignoring it. Removing the redundant command was enough.
  • USB network gadget (NCM): three NULL-pointer bugs in f_ncm.c. After the fix the kernel side works, but ADB and the network gadget still can't run together: UsbService.setCurrentFunctions in the Android framework explicitly rejects that combination (IllegalArgumentException). That's no kernel bug but a deliberate Android decision, and it can't be fixed there.

Every build (baseline, individual fixes, final consolidated kernel) is verified with a SHA-256 checksum before flashing and validated on the real phone. There are no "try it and see" steps. Using Magisk instead of TWRP means I can always return to the original boot.img, which I keep set aside for exactly this purpose.

DeX doesn't work, and why I gave up on it

On the stock Samsung ROM, DeX worked fine through the same dock and cable. On the LineageOS/NetHunter kernel it didn't, and it took me a while to understand why, until I found the exact mechanism in dmesg.

The DisplayPort link comes up and video partly starts (1920×1080 negotiates correctly), but right at HDCP 2.2 negotiation (dpcd_read_for_hdcp22) comes an AUX_TRAN_START waiting timeout. It repeats every ~200-230 seconds as the whole cycle disconnects and starts over. Turning off HDCP on the monitor had no effect, which ruled out the monitor and the cable. The problem is purely in the phone.

There's no runtime switch to get around it (I tried sysfs nodes and kernel module parameters, with no luck). A fix would need changes directly in the DisplayPort driver, probably in its communication with TrustZone/TEE, since HDCP 2.2 keys normally go through a secure world applet that may not initialise properly on a custom boot. And even with HDCP fixed, LineageOS wouldn't have Samsung's proprietary DeX framework, so the real DeX experience would remain out of reach.

Rather than chase this particular bug, I picked a different goal: not DeX, but native Android desktop mode. It sidesteps the problem, because it doesn't depend on HDCP negotiation in the same way. More on that below.

A native Kali chroot

On top of the kernel runs a full Kali NetHunter chroot (not a stripped-down Termux rootless substitute), with two fixes of my own that make it really usable:

  • Mountpoint detection in the app's own scripts (bootkali_init, killkali) reported false negatives, because busybox mountpoint -q isn't reliable in this environment. I replaced it with reading /proc/self/mountinfo directly.
  • I bound Android's cgroup2 (/sys/fs/cgroup) directly into the chroot. Any container runtime inside the chroot needs it, and without it Docker or Podman won't start there at all.

Added to that are persistent SSH access by public key (password and interactive login disabled) and a custom USB gadget configured on every boot: a ConfigFS HID keyboard, ADB and a serial console together on one port. Autostarting sshd in the chroot had its own, less obvious problem: /data/data/com.offsec.nethunter is encrypted storage that doesn't exist until the phone has been unlocked at least once after boot, and that's a human action with no fixed time limit. The original script with its 5-minute timeout gave up too early. It now waits for the unlock with no time limit.

Android desktop mode from an S20 Ultra running on an external monitor

Android desktop mode (not Samsung DeX)

This was the last and most interesting part. LineageOS doesn't include Samsung's proprietary DeX framework at all, so the goal was different: to get native AOSP "connected displays" windows on an external monitor through a USB-C dock, the way Android itself is meant to do it, not as a DeX replacement.

It took two things together: a persistent ro.build.characteristics=tablet (set permanently through Magisk) and the decisive Desktop Experience developer-preview switch (persist.wm.debug.desktop_experience_devopts). After turning it on and restarting system_server, the external monitor got real windows, a taskbar and its own desktop, independent of the phone's locked screen, even after rebooting the phone.

I used the Smart Dock app as the launcher for icons on the external desktop, but the AOSP shell itself controls the windows and taskbar. The two launchers overlapped at first: Smart Dock could draw a duplicate overlay panel over what AOSP had created. I solved it with a small watchdog script that detects the duplicate panel (by parsing dumpsys window windows) and sends it a synthetic downward swipe, the same gesture a finger makes when closing it by hand. I chose that deliberately over a tap on a button, because a swipe has no toggle effect that could flip the state back by mistake.

The result: the phone works as a standalone device with its own locked screen and, when plugged into the dock, also runs a full second desktop on the external monitor, with its own launcher, desktop icons and the mouse routed to the right display.

What's done and what isn't

Working and verified: Wi-Fi, Bluetooth, ADB, Magisk root, normal Android boot through a reboot, the native Kali terminal, Nmap/Metasploit/OpenSSH in the chroot, external USB Wi-Fi adapters with monitor mode, and desktop mode surviving a phone restart.

What doesn't work, or is a deliberate compromise:

  • The internal Broadcom Wi-Fi card doesn't support monitor mode at all, so you need an external adapter.
  • The USB mass storage gadget driver (CONFIG_USB_F_MASS_STORAGE) is missing, so the phone can't offer files as a USB disk.
  • The UVC driver is missing. External USB cameras give working sound and storage, but no video.
  • I don't switch USB Arsenal/HID live, because that would cut the only ADB channel.
  • Opening the NetHunter app changes SELinux from Enforcing to Permissive. That's a deliberate security compromise, not an oversight.
  • DeX (above) stays broken, and will probably remain so.

Source code

The whole build (scripts, chroot and module fixes, on-device automation and the field manual) is public on GitHub: github.com/stanleysvk87/s20-nethunter-port. Boot images are there as a GitHub Release, since they exceed the file size limit for the repo itself.

I'm not writing this only for myself. If someone has the same phone and hit the same gap, I hope it saves them some time.