Skip to content

Instantly share code, notes, and snippets.

@NotKit
Created October 10, 2025 08:09
Show Gist options
  • Select an option

  • Save NotKit/3e4a9af7328300631cd7be5fca825205 to your computer and use it in GitHub Desktop.

Select an option

Save NotKit/3e4a9af7328300631cd7be5fca825205 to your computer and use it in GitHub Desktop.
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Copyright (C) 2025 TheKit
# SPDX-License-Identifier: GPL-3.0-or-later
"""
Generate libinput quirks for devices with problematic kernel drivers.
On some devices (commonly OnePlus/OPPO/Realme), the touchscreen kernel driver
reports certain absolute axes (ABS_MT_WIDTH_MAJOR, ABS_MT_PRESSURE, etc.) with
min == max values, which causes libinput to reject the device with a "kernel bug"
error.
This script automatically detects such devices and generates a libinput
quirks configuration to disable the problematic axes.
"""
import os
import sys
import glob
import ctypes
import fcntl
import subprocess
from ctypes import c_int, Structure
# ioctl constants
_IOC_NRBITS = 8
_IOC_TYPEBITS = 8
_IOC_SIZEBITS = 14
_IOC_DIRBITS = 2
_IOC_NRSHIFT = 0
_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS
_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS
_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS
_IOC_NONE = 0
_IOC_WRITE = 1
_IOC_READ = 2
def _IOC(dir, type, nr, size):
return (
(dir << _IOC_DIRSHIFT) |
(type << _IOC_TYPESHIFT) |
(nr << _IOC_NRSHIFT) |
(size << _IOC_SIZESHIFT)
)
def _IOR(type, nr, size):
return _IOC(_IOC_READ, type, nr, size)
# Input event subsystem constants
EV_ABS = 0x03
# Absolute axis codes we're interested in
ABS_PRESSURE = 0x18
ABS_MT_TOUCH_MAJOR = 0x30
ABS_MT_TOUCH_MINOR = 0x31
ABS_MT_WIDTH_MAJOR = 0x32
ABS_MT_WIDTH_MINOR = 0x33
ABS_MT_PRESSURE = 0x3a
# Absolute axis information structure
class input_absinfo(Structure):
_fields_ = [
("value", c_int),
("minimum", c_int),
("maximum", c_int),
("fuzz", c_int),
("flat", c_int),
("resolution", c_int),
]
# ioctl for getting absolute axis information
EVIOCGABS = lambda x: _IOR(ord('E'), 0x40 + x, ctypes.sizeof(input_absinfo))
# ioctl for getting device name
EVIOCGNAME_LEN = 256
EVIOCGNAME = _IOR(ord('E'), 0x06, EVIOCGNAME_LEN)
# Bit test macros
EVIOCGBIT = lambda ev, len: _IOC(_IOC_READ, ord('E'), 0x20 + ev, len)
def test_bit(bit, array):
"""Test if a bit is set in a byte array."""
return (array[bit // 8] & (1 << (bit % 8))) != 0
def get_device_name(fd):
"""Get the name of an input device."""
name_buffer = ctypes.create_string_buffer(EVIOCGNAME_LEN)
try:
fcntl.ioctl(fd, EVIOCGNAME, name_buffer)
return name_buffer.value.decode('utf-8', errors='ignore').rstrip('\x00')
except (OSError, IOError):
pass
return None
def get_abs_info(fd, axis):
"""Get absolute axis information."""
absinfo = input_absinfo()
try:
fcntl.ioctl(fd, EVIOCGABS(axis), absinfo)
return absinfo
except (OSError, IOError):
pass
return None
def has_event_type(fd, event_type):
"""Check if device supports a given event type."""
# Allocate enough bytes to hold EV_MAX bits
evbits = ctypes.create_string_buffer(32)
try:
fcntl.ioctl(fd, EVIOCGBIT(0, len(evbits)), evbits)
return test_bit(event_type, evbits.raw)
except (OSError, IOError):
pass
return False
def has_abs_axis(fd, axis):
"""Check if device supports a specific absolute axis."""
# Allocate enough bytes to hold ABS_MAX bits (0x3f = 63)
absbits = ctypes.create_string_buffer(8)
try:
fcntl.ioctl(fd, EVIOCGBIT(EV_ABS, len(absbits)), absbits)
return test_bit(axis, absbits.raw)
except (OSError, IOError):
pass
return False
def check_device(device_path):
"""
Check if a device has problematic axis values.
Returns a tuple (device_name, problematic_axes) where problematic_axes
is a list of axis names that have min == max.
"""
try:
fd = os.open(device_path, os.O_RDONLY | os.O_NONBLOCK)
except (OSError, IOError):
return None, []
try:
# Get device name
device_name = get_device_name(fd)
if not device_name:
return None, []
# Check if device supports absolute axes
if not has_event_type(fd, EV_ABS):
return None, []
# Check for problematic axes
axes_to_check = {
ABS_PRESSURE: 'ABS_PRESSURE',
ABS_MT_TOUCH_MAJOR: 'ABS_MT_TOUCH_MAJOR',
ABS_MT_TOUCH_MINOR: 'ABS_MT_TOUCH_MINOR',
ABS_MT_WIDTH_MAJOR: 'ABS_MT_WIDTH_MAJOR',
ABS_MT_WIDTH_MINOR: 'ABS_MT_WIDTH_MINOR',
ABS_MT_PRESSURE: 'ABS_MT_PRESSURE',
}
problematic = []
for axis_code, axis_name in axes_to_check.items():
# First check if the device actually supports this axis
if not has_abs_axis(fd, axis_code):
continue
absinfo = get_abs_info(fd, axis_code)
if absinfo and absinfo.minimum == absinfo.maximum:
problematic.append(axis_name)
return device_name, problematic
finally:
os.close(fd)
def is_mounted_ro(path):
"""Check if a path is on a read-only mounted filesystem."""
try:
# Find the mount point for the path
abs_path = os.path.abspath(path)
# Read /proc/mounts to find the mount point
with open('/proc/mounts', 'r') as f:
mounts = f.readlines()
# Find the longest matching mount point
best_match = None
best_match_len = 0
for line in mounts:
parts = line.split()
if len(parts) < 4:
continue
mount_point = parts[1]
mount_options = parts[3].split(',')
# Check if our path is under this mount point
if abs_path.startswith(mount_point):
if len(mount_point) > best_match_len:
best_match = mount_point
best_match_len = len(mount_point)
is_ro = 'ro' in mount_options
return is_ro if best_match else False
except (OSError, IOError):
return False
def remount(path, mode):
"""
Remount a filesystem with the specified mode ('ro' or 'rw').
Args:
path: Path on the filesystem to remount
mode: Either 'ro' (read-only) or 'rw' (read-write)
Returns:
True if remount was successful, False otherwise
"""
try:
# Find the actual mount point
abs_path = os.path.abspath(path)
with open('/proc/mounts', 'r') as f:
mounts = f.readlines()
mount_point = None
best_match_len = 0
for line in mounts:
parts = line.split()
if len(parts) < 2:
continue
mp = parts[1]
if abs_path.startswith(mp) and len(mp) > best_match_len:
mount_point = mp
best_match_len = len(mp)
if mount_point:
subprocess.run(['mount', '-o', f'remount,{mode}', mount_point],
check=True, capture_output=True)
return True
except (OSError, IOError, subprocess.CalledProcessError) as e:
print(f"WARNING: Could not remount {path} as {mode}: {e}", file=sys.stderr)
return False
def generate_quirks_config(devices):
"""Generate libinput quirks configuration for problematic devices."""
if not devices:
return None
config_lines = [
"# Auto-generated libinput quirks configuration",
"# Generated by lxc-android-config",
"#",
"# This file works around kernel driver bugs where certain absolute",
"# axes have min == max values, causing libinput to reject the device.",
"",
]
for device_name, axes in devices.items():
if not axes:
continue
# Create a quirk section for this device
config_lines.append(f"[{device_name}]")
config_lines.append(f"MatchName={device_name}")
# Disable the problematic event codes
disabled_codes = ";".join(axes)
config_lines.append(f"AttrEventCodeDisable={disabled_codes};")
config_lines.append("")
return "\n".join(config_lines)
def main():
"""Main function."""
# Check for verbose mode
verbose = '-v' in sys.argv or '--verbose' in sys.argv
# Scan all event devices
problematic_devices = {}
event_devices = glob.glob('/dev/input/event*')
if not event_devices:
print("INFO: No input devices found", file=sys.stderr)
return 0
for device_path in sorted(event_devices):
device_name, problematic_axes = check_device(device_path)
if device_name:
if problematic_axes:
print(f"Found problematic device: {device_name} ({device_path})")
print(f" Problematic axes: {', '.join(problematic_axes)}")
problematic_devices[device_name] = problematic_axes
elif verbose:
print(f"Checked device: {device_name} ({device_path}) - OK")
# Generate quirks configuration
if problematic_devices:
config_content = generate_quirks_config(problematic_devices)
# Ensure directory exists
quirks_dir = '/etc/libinput'
# Check if we need to remount /etc as read-write
was_ro = is_mounted_ro(quirks_dir)
if was_ro:
remount(quirks_dir, 'rw')
try:
os.makedirs(quirks_dir, exist_ok=True)
quirks_file = os.path.join(quirks_dir, 'local-overrides.quirks')
# Write configuration
with open(quirks_file, 'w') as f:
f.write(config_content)
print(f"Generated quirks configuration: {quirks_file}")
return_code = 0
except (OSError, IOError) as e:
print(f"ERROR: Could not write quirks file: {e}", file=sys.stderr)
return_code = 1
finally:
# Remount as read-only if it was originally read-only
if was_ro:
remount(quirks_dir, 'ro')
return return_code
else:
print("No problematic devices found, no quirks needed.")
return 0
if __name__ == '__main__':
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment