All work

Sep 2026 Solo build: diagnosis, fixes, docs

Surface Pro 5 on Ubuntu

Fixes and a setup guide for running Ubuntu 26.04 on a Surface Pro 5 as a tablet: multitouch that works, a touchscreen that survives boot and snap installs, a kernel pin that does not go stale, and working power and volume buttons.

Linux Ubuntu Bash systemd udev
surface-pro — ./scripts/check.sh
./scripts/check.sh
scripts/
  • check.sh

    Read-only health check.

  • install.sh

    Safe to re-run, backs up what it replaces.

  • uninstall.sh

    Removes what install.sh added.

  • tablet-desktop.sh

    Per-user GNOME settings, no sudo.

Surface Pro 5 on Ubuntu is the set of fixes I needed to use a 2017 Surface Pro 5 (model 1796) as a tablet on Ubuntu 26.04. The linux-surface kernel gets the hardware working, but it does not make the machine behave like a tablet. After a clean install I had a touchscreen that stopped working after installing a snap, two-finger touches that zoomed, scrolled and clicked at once, no touch at all after some reboots, a power button and volume rocker that did nothing, and a boot setup that would pick the wrong kernel after the next update. Each fix is a small file under etc/ with a doc that explains the cause and how to check it, installed by scripts that can show their changes first and remove them again.

Touch bugs fixed
3
Tested kernel
6.19.8-surface-3
Scripts
4
OS
Ubuntu 26.04
Stack
Ubuntu 26.04 linux-surface iptsd Bash systemd udev GRUB libinput GNOME gsettings

Two paths to one touchscreen

IPTS, the touch hardware in this Surface, does not send finger positions. It sends a raw capacitance heatmap. iptsd, a daemon from the linux-surface project, turns that heatmap into touches and hands them back to the kernel as two virtual devices: a multitouch screen with 17 slots and a stylus with pressure and tilt.

The kernel also exposes a fallback touchscreen that always works but reports one finger with no contact ID. So there are two ways to read the same panel. The good one needs a daemon running, and the basic one cannot count past one. Every touch bug I hit came from the gap between them.

Three touch bugs, each traced to a cause

Each bug has a doc with the evidence and a command to confirm the fix. The fixes are independent, but hiding the duplicate device removes the single-touch fallback, so when iptsd is not running there is no touch at all. That makes the boot fix required, so they install together.

With touch stable, three-finger swipes still needed several tries. The device was not the limit: the virtual touchscreen has 17 slots and reports a panel size close to the real one. iptsd was dropping contacts mid-swipe, so a drop-in config raises the per-frame movement limit from 2 cm to 3 cm, allows a stretched contact aspect of 3.0 instead of 2.5, and lowers the detection thresholds. I left the contact size limits alone, because widening them weakens palm rejection and helps nothing.

A snap install kills touch
The packaged iptsd udev rule only matches add events. snapd re-triggers udev with change events, the systemd tag drops off the device, and systemd stops iptsd. A second rule handles change events and adds to the packaged one instead of replacing it.
Two fingers counted twice
libinput read both the raw single-touch device and the iptsd virtual device, so two fingers gave a pinch, a scroll and phantom clicks at once. A udev rule sets LIBINPUT_IGNORE_DEVICE on names matching "IPTS *". The space in the glob is what keeps the "IPTSD" virtual devices visible.
Touch is a coin flip at boot
iptsd asked for a mode switch about 280 ms after the device appeared, before the Intel Management Engine was ready. The switch failed and left the controller stuck until the driver was reloaded. A systemd drop-in waits 10 seconds in the first minute of uptime and 2 seconds after that, so a resume from suspend does not wait the full ten seconds.
Recovery when it still fails
If retries do not help, a recovery unit stops iptsd, reloads the ipts module and lets udev start it again. It is limited to three resets in ten minutes and matches devices by glob, because the hidraw number moved between reloads.

A kernel pin that keeps up with updates

Ubuntu’s stock kernel version-sorts above the linux-surface kernel, so it owns GRUB menu entry 0. The only way to boot the surface kernel by default is to name it in GRUB_DEFAULT, and that name includes the exact version. The next linux-surface update makes the name match nothing, GRUB falls back to entry 0, and the tablet boots a kernel with no touchscreen or pen support.

A kernel hook fixes the pin at the source. On every kernel install it finds the newest surface kernel by version sort and rewrites GRUB_DEFAULT to match, and a link in postrm.d does the same when a kernel is removed. It is named zz-surface-grub-default so run-parts runs it just before Ubuntu’s zz-update-grub, which means update-grub sees the new value in the same dpkg run.

Two details keep it from doing harm. It uses if blocks instead of && chains, because under set -e a false test can exit non-zero and abort a kernel install. If no surface kernel is installed, it leaves the pin alone rather than guess. It backs up /etc/default/grub before rewriting it, and with nothing to change it prints nothing and exits 0. The installer also sets a hidden menu with a one-second timeout, so holding Esc during boot still reaches the stock kernel as a fallback.

Power and volume buttons with no driver

On a clean install the power button and volume rocker did nothing, and nothing showed in evtest or the journal. The kernel had no input device for them. The hardware was there: the ACPI device MSHW0040 reported status 15, present and functioning, but no driver was bound and dmesg showed no probe attempt at all. Two drivers claim that ID, and the handover from surfacepro3_button to soc_button_array never happened.

Writing the device name to the platform bus drivers_probe file made soc_button_array bind right away and created two gpio-keys devices, one for the volume rocker and one for power. A oneshot systemd unit does that write at every boot. It only runs when the device exists, so it is skipped on other hardware, and it does nothing if a driver is already bound. I tested it by unbinding the driver, confirming the buttons were dead, and restarting the unit to bring them back.

A short press now suspends instead of opening a menu. Some limits are hardware or GNOME, not configuration: this machine has no S3 deep sleep, only s2idle, and a long press cannot open a power menu because GNOME holds the power-key inhibitor lock that systemd always honors. I documented those as limits instead of chasing workarounds.

Scripts that look before they change anything

The repo has four scripts that share one helper library. The installer can apply one part at a time, shows every change with --dry-run, is safe to re-run, and backs up any file it replaces with a timestamp. Every config file starts with a comment that explains the problem it fixes.

The desktop script refuses to run under sudo. gsettings under sudo writes root’s settings, reports success, and changes nothing on your desktop. I lost an afternoon to that before building the guard.

check.sh
Read-only health check: kernel, Secure Boot key, iptsd state, duplicate-device suppression, a stale GRUB pin, button binding, sleep states and failed units. Run it before installing and again after the reboot.
install.sh
Installs touch, grub and buttons, together or one at a time, with --dry-run, timestamped backups, and checks such as systemd-analyze verify and run-parts --test after the files land.
uninstall.sh
Removes each part, with its own --dry-run, and reloads udev and systemd. Backups stay where they were written.
tablet-desktop.sh
Per-user GNOME settings: on-screen keyboard, power button to suspend, text scale and screen blanking. Has --show and --revert, and refuses to run as root.
Back to all work