fix(udev): restrict mode switching to intended devices and events

Take the aic.rules portion of #129: restrict 1111:1111 switching to
88M80 USB devices, target the event's bus/device address, and eject only
whole disks on add. Add selection fixtures and document the product
restriction. Keep installer paths and existing firmware handling unchanged.

Co-authored-by: rtk94 <richardtknepp@gmail.com>
(cherry picked from commit e2c5835767)
This commit is contained in:
Shen Mintao committed 2026-11-02 14:32:11 +08:00
1 parent 20fb7181e6
commit 3a71c7188b
3 files changed
+89 -11

No files matched your search

+5
View File
@@ -85,6 +85,11 @@ commands. Existing users must rerun `sudo ./install.sh` to replace the installed
usb_modeswitch configuration, then unplug the adapter completely and plug it
back in.
Automatic switching requires the USB product string to start with `88M80`.
Unrelated flash disks also use `1111:1111`, so that USB ID alone is not enough.
If your adapter reports a different product string, verify its identity before
trying the manual command below.
If automatic switching does not occur, try the same sequence manually:
```bash
+15 -11
View File
@@ -1,14 +1,18 @@
# AIC8800 USB mass-storage mode switch rules
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5721", SYMLINK+="aicudisk", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5722", SYMLINK+="aicudisk", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5723", SYMLINK+="tendaudisk", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5724", SYMLINK+="ugreenax900", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5725", SYMLINK+="tendaudiskv2", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5726", SYMLINK+="tendaudiskv3", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5727", SYMLINK+="tendaudiskv4", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="572a", SYMLINK+="tendaudiskv5", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="572c", SYMLINK+="cudydiskv2", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="572f", SYMLINK+="aicudisk572f", RUN+="/usr/bin/eject /dev/%k"
ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5721", SYMLINK+="aicudisk", RUN+="/usr/bin/eject /dev/%k"
ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5722", SYMLINK+="aicudisk", RUN+="/usr/bin/eject /dev/%k"
ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5723", SYMLINK+="tendaudisk", RUN+="/usr/bin/eject /dev/%k"
ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5724", SYMLINK+="ugreenax900", RUN+="/usr/bin/eject /dev/%k"
ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5725", SYMLINK+="tendaudiskv2", RUN+="/usr/bin/eject /dev/%k"
ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5726", SYMLINK+="tendaudiskv3", RUN+="/usr/bin/eject /dev/%k"
ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5727", SYMLINK+="tendaudiskv4", RUN+="/usr/bin/eject /dev/%k"
ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="572a", SYMLINK+="tendaudiskv5", RUN+="/usr/bin/eject /dev/%k"
ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="572c", SYMLINK+="cudydiskv2", RUN+="/usr/bin/eject /dev/%k"
ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="572f", SYMLINK+="aicudisk572f", RUN+="/usr/bin/eject /dev/%k"
# AIC8800D80 "Pandora" clone: switch 1111:1111 to its operational USB ID.
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1111", ATTR{idProduct}=="1111", RUN+="/bin/sh -c 'if [ -x /usr/sbin/usb_modeswitch ]; then exec /usr/sbin/usb_modeswitch -c /etc/usb_modeswitch.d/1111:1111; elif [ -x /usr/bin/usb_modeswitch ]; then exec /usr/bin/usb_modeswitch -c /etc/usb_modeswitch.d/1111:1111; fi'"
# 1111:1111 is a placeholder ID that other devices also report (some AIC
# dongles expose a separate 1111:1111 "FLASH" driver disk next to an
# already working Wi-Fi function), so only devices whose product string is
# 88M80 are switched, and usb_modeswitch is pointed at this exact device.
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="1111", ATTR{idProduct}=="1111", ATTR{product}=="88M80*", RUN+="/bin/sh -c 'if [ -x /usr/sbin/usb_modeswitch ]; then exec /usr/sbin/usb_modeswitch -b $attr{busnum} -g $attr{devnum} -c /etc/usb_modeswitch.d/1111:1111; elif [ -x /usr/bin/usb_modeswitch ]; then exec /usr/bin/usb_modeswitch -b $attr{busnum} -g $attr{devnum} -c /etc/usb_modeswitch.d/1111:1111; fi'"
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Check device/event selection using fixtures; does not switch real devices.
This models the basic equality/glob predicates used by aic.rules. Run
udevadm verify separately to validate syntax with the real udev parser.
"""
from fnmatch import fnmatchcase
from pathlib import Path
import re
import unittest
RULES = (Path(__file__).resolve().parents[2] / "aic.rules").read_text().splitlines()
def matching_rules(device):
result = []
for rule in RULES:
if not rule.strip() or rule.lstrip().startswith("#"):
continue
predicates = re.findall(r'([A-Z]+(?:\{[^}]+\})?)=="([^"]*)"', rule)
if predicates and all(fnmatchcase(device.get(key, ""), value) for key, value in predicates):
result.append(rule)
return result
class UdevTests(unittest.TestCase):
def pandora(self, **changes):
device = {"ACTION": "add", "SUBSYSTEM": "usb", "ENV{DEVTYPE}": "usb_device",
"ATTR{idVendor}": "1111", "ATTR{idProduct}": "1111", "ATTR{product}": "88M80"}
device.update(changes)
return device
def test_88m80_uses_event_bus_and_device_numbers(self):
for product in ("88M80", "88M80 USB WiFi"):
with self.subTest(product=product):
matches = matching_rules(self.pandora(**{"ATTR{product}": product}))
self.assertEqual(len(matches), 1)
# Both possible usb_modeswitch executable locations target this event's device.
self.assertEqual(matches[0].count("-b $attr{busnum} -g $attr{devnum}"), 2)
def test_generic_1111_flash_disk_is_not_switched(self):
for product in ("FLASH UDISK 2.30", "USB DISK", ""):
with self.subTest(product=product):
self.assertEqual(matching_rules(self.pandora(**{"ATTR{product}": product})), [])
def test_wrong_ids_interfaces_and_non_add_events_are_not_switched(self):
for changes in ({"ACTION": "change"}, {"ACTION": "remove"},
{"ENV{DEVTYPE}": "usb_interface"}, {"ATTR{idVendor}": "2222"},
{"ATTR{idProduct}": "8d80"}):
with self.subTest(changes=changes):
self.assertEqual(matching_rules(self.pandora(**changes)), [])
def test_eject_only_on_add_of_a_whole_supported_disk(self):
for product in ("5721", "5722", "5723", "5724", "5725", "5726", "5727", "572a", "572c", "572f"):
disk = {"ACTION": "add", "SUBSYSTEM": "block", "ENV{DEVTYPE}": "disk", "KERNEL": "sdb",
"ATTRS{idVendor}": "a69c", "ATTRS{idProduct}": product}
with self.subTest(product=product):
matches = matching_rules(disk)
self.assertEqual(len(matches), 1)
self.assertIn('/usr/bin/eject /dev/%k', matches[0])
for changes in ({"ACTION": "change"}, {"ACTION": "remove"},
{"ENV{DEVTYPE}": "partition", "KERNEL": "sdb1"}):
self.assertEqual(matching_rules(dict(disk, **changes)), [])
if __name__ == "__main__":
unittest.main(verbosity=2)