NetBSD で USB デバイスを強制的に ugen(4) として attach したい

通常は uftdi(4) とか uhid(4) として認識される USB デバイスを ugen(4) として認識させたい事ってありませんか?

そうですか、ありませんか…。

はじめに

これは「NetBSD でも YubiKey を使って二要素認証したい」の以下の文章を説明するための記事です。

ここで ukbd(4) が attach されていないと動作モードの変更が必要になるのですが、素の NetBSD カーネルだと動作モードの変更が難しいので後述することにします、しないかもしれません。

なんで ugen(4) にしたいの?

まずは libusb のソースを見てもらおうか。話はそれからだ。

int
netbsd_get_device_list(struct libusb_context * ctx,
    struct discovered_devs **discdevs)
{
    struct libusb_device *dev;
    struct device_priv *dpriv;
    struct usb_device_info di;
    unsigned long session_id;
    char devnode[16];
    int fd, err, i;

    usbi_dbg("");

    /* Only ugen(4) is supported */
    for (i = 0; i < USB_MAX_DEVICES; i++) {
        /* Control endpoint is always .00 */
        snprintf(devnode, sizeof(devnode), "/dev/ugen%d.00", i);

libusb を使えばデバイスドライバを書かなくてもユーザーランドプログラムから USB デバイスを制御できるのですが、残念ながら NetBSD 上では libusb は上記ソースに記載されている通り ugen(4) として認識されている USB デバイスしか扱えないのです。

そんな訳で uhid(4) や ukbd(4) として認識されている YubiKey 4 を libusb 経由で YubiKey を制御する ykpersonalize や ykinfo といったプログラムは制御する以前にそもそも YubiKey デバイスを見つけられません。

nonaka@koharu$ dmesg
(snip...)
uhidev2 at uhub2 port 1 configuration 1 interface 0
uhidev2: Yubico Yubikey NEO OTP+U2F, rev 2.00/3.43, addr 6, iclass 3/1
ukbd1 at uhidev2: 8 modifier keys, 6 key codes
wskbd2 at ukbd1 mux 1
wskbd2: connecting to wsdisplay0
uhidev3 at uhub2 port 1 configuration 1 interface 1
uhidev3: Yubico Yubikey NEO OTP+U2F, rev 2.00/3.43, addr 6, iclass 3/0
uhid0 at uhidev3: input=64, output=64, feature=0
nonaka@koharu$ sudo ykinfo -v
Yubikey core error: no yubikey present

本来ならば ukbd(4) などの USB デバイスドライバはすべからく ugen(4) として振る舞えるべき、もしくはインタフェースを持つべきだとは思うのですが、それを実現するにはちょっと長い道のりとなります。

なのでここではデバイスドライバの match/attach 処理に小細工をして、USB デバイスを接続した際に正しいデバイスドライバではなく ugen(4) として認識させようとします。

デバイスドライバ match/attach 処理

もの凄くざっくりとした説明

NetBSD ではあるバス(PCI とか USB とか)に接続されているデバイスドライバを attach する際には、該当バスに接続できるすべてのデバイスドライバの match 関数を呼び出します。

match 関数は PCI や USB であれば Vendor ID, Product ID といったバス固有の情報を受け取り、その情報から該当デバイスドライバで取り扱うデバイスであると判断したら match 関数の戻り値として 1 以上の整数値を返します。

全てのデバイスドライバの match 関数を呼び終わった後で 0 では無い一番大きな値を返したデバイスドライバの attach 関数を呼び出してデバイスドライバを attach します。

まあ、要するに ugen(4) として attach してほしいデバイスには ugen(4) の match 関数で他のデバイスドライバよりも大きな値を戻り値として返せば良いだけです。

USB スタック固有事情

実は USB スタックでは この match 関数で返すべき値が用意されています。

/* Match codes. */
#define UMATCH_HIGHEST                 15
/* First five codes is for a whole device. */
#define UMATCH_VENDOR_PRODUCT_REV          14
#define UMATCH_VENDOR_PRODUCT              13
#define UMATCH_VENDOR_DEVCLASS_DEVPROTO            12
#define UMATCH_DEVCLASS_DEVSUBCLASS_DEVPROTO       11
#define UMATCH_DEVCLASS_DEVSUBCLASS            10
/* Next six codes are for interfaces. */
#define UMATCH_VENDOR_PRODUCT_REV_CONF_IFACE        9
#define UMATCH_VENDOR_PRODUCT_CONF_IFACE        8
#define UMATCH_VENDOR_IFACESUBCLASS_IFACEPROTO      7
#define UMATCH_VENDOR_IFACESUBCLASS             6
#define UMATCH_IFACECLASS_IFACESUBCLASS_IFACEPROTO  5
#define UMATCH_IFACECLASS_IFACESUBCLASS             4
#define UMATCH_IFACECLASS               3
#define UMATCH_IFACECLASS_GENERIC           2
/* Generic driver */
#define UMATCH_GENERIC                  1
/* No match */
#define UMATCH_NONE                     0

という訳で他のデバイスとして attach して貰いたくない場合には ugen(4) で該当デバイスの場合に UMATCH_HIGHEST を返すようにすれば良いという事になります。

ugen(4) match 関数

それでは ugen(4) の match 関数である ugen_match() 関数を見てみましょう。

/* toggle to control attach priority. -1 means "let autoconf decide" */
int ugen_override = -1;

int
ugen_match(device_t parent, cfdata_t match, void *aux)
{
    struct usb_attach_arg *uaa = aux;
    int override;

    if (ugen_override != -1)
        override = ugen_override;
    else
        override = match->cf_flags & 1;

    if (override)
        return (UMATCH_HIGHEST);
    else if (uaa->usegeneric)
        return (UMATCH_GENERIC);
    else
        return (UMATCH_NONE);
}

既に UMATCH_HIGHEST を返すコードがありますね。

struct usb_attach_arg が先ほど説明したバス固有のデバイス情報になります。

struct usb_attach_arg {
    int            port;
    int            vendor;
    int            product;
    int            release;
    usbd_device_handle  device; /* current device */
    int            class, subclass, proto;
    int            usegeneric;
};

struct usb_attach_arg に接続されたデバイスの Vendor ID と Product ID があるので、この二つが ugen(4) として attach したいデバイスの値と一致したら UMATCH_HIGHEST を返すコードを追加すれば良いですかね。

後は ugen(4) として attach したいデバイスの Vendor ID と Product ID をどうやって指定するかという事になります。

Linux であれば /proc 経由で良いのでしょうが、NetBSD では何年前かはまで知りませんが今時は sysctl(8) 経由でユーザランドから設定できるようにするらしいです。

完成

良くあるように実際の処理よりもユーザインタフェース部分の方が処理が多い差分になりました。仕方ないね

USB デバイスの Vendor ID と Product ID はデバイスを接続した状態で usbdevs -v で表示されます。

nonaka@koharu$ sudo usbdevs -v
Controller /dev/usb0:
addr 0: high speed, self powered, config 1, xHCI Root Hub(0x0000), NetBSD(0x0000), rev 1.00
 port 1 powered
 port 2 powered
 port 3 powered
 port 4 powered
 port 5 powered
 port 6 powered
 port 7 powered
 port 8 powered
 port 9 powered
Controller /dev/usb1:
addr 1: high speed, self powered, config 1, EHCI root hub(0x0000), Intel(0x8086), rev 1.00
 port 1 addr 2: high speed, self powered, config 1, product 8000(0x8000), Intel(0x8087), rev 0.04
  port 1 addr 6: full speed, power 30 mA, config 1, Yubikey NEO OTP+U2F(0x0114), Yubico(0x1050), rev 3.43
  port 2 powered
  port 3 powered
  port 4 powered
  port 5 addr 3: full speed, self powered, config 1, product 0a2a(0x0a2a), Intel(0x8087), rev 0.01
  port 6 addr 4: high speed, power 200 mA, config 1, BisonCam, NB Pro(0x024b), vendor 5986(0x5986), rev 6.08
  port 7 addr 5: full speed, power 100 mA, config 1, ThinkPad Compact USB Keyboard with TrackPoint(0x6047), Lenovo(0x17ef), rev 3.00
  port 8 powered
 port 2 powered

実際に使ってみた

$ dmesg
(snip...)
uhidev2 at uhub2 port 1 configuration 1 interface 0
uhidev2: Yubico Yubikey NEO OTP+U2F, rev 2.00/3.43, addr 6, iclass 3/1
ukbd1 at uhidev2: 8 modifier keys, 6 key codes
wskbd2 at ukbd1 mux 1
wskbd2: connecting to wsdisplay0
uhidev3 at uhub2 port 1 configuration 1 interface 1
uhidev3: Yubico Yubikey NEO OTP+U2F, rev 2.00/3.43, addr 6, iclass 3/0
uhid0 at uhidev3: input=64, output=64, feature=0
$ sysctl hw.ugen
hw.ugen.override = -1
hw.ugen.forced_attach.vendor = 65535
hw.ugen.forced_attach.product = 65535
hw.ugen.forced_attach.addr = -1
hw.ugen.forced_attach.add = 0
hw.ugen.forced_attach.delete = 0
hw.ugen.forced_attach.clear = 0
hw.ugen.forced_attach.list = 
$ sudo sysctl -w hw.ugen.forced_attach.vendor=0x1050
hw.ugen.forced_attach.vendor: 65535 -> 4176
$ sudo sysctl -w hw.ugen.forced_attach.product=0x114
hw.ugen.forced_attach.product: 65535 -> 276
$ sudo sysctl -w hw.ugen.forced_attach.add=1
hw.ugen.forced_attach.add: 0 -> 1
$ sysctl hw.ugen
hw.ugen.override = -1
hw.ugen.forced_attach.vendor = 65535
hw.ugen.forced_attach.product = 65535
hw.ugen.forced_attach.addr = -1
hw.ugen.forced_attach.add = 0
hw.ugen.forced_attach.delete = 0
hw.ugen.forced_attach.clear = 0
hw.ugen.forced_attach.list = [0x1050 0x0114 -1]
(ここでデバイスを一旦抜いてから接続しなおす)
$ dmesg
(snip...)
wskbd2: disconnecting from wsdisplay0
wskbd2: detached
ukbd1: detached
uhidev2: detached
uhidev2: at uhub2 port 1 (addr 6) disconnected
uhid0: detached
uhidev3: detached
uhidev3: at uhub2 port 1 (addr 6) disconnected
ugen0 at uhub2 port 1
ugen0: Yubico Yubikey NEO OTP+U2F, rev 2.00/3.43, addr 6
$ sudo ykinfo -v
version: 3.4.3

使い方

デバイス追加
$ sudo sysctl -w hw.ugen.forced_attach.vendor=0x1050
hw.ugen.forced_attach.vendor: 65535 -> 4176
$ sudo sysctl -w hw.ugen.forced_attach.product=0x114
hw.ugen.forced_attach.product: 65535 -> 276
$ sudo sysctl -w hw.ugen.forced_attach.add=1
hw.ugen.forced_attach.add: 0 -> 1
$ sysctl hw.ugen
hw.ugen.override = -1
hw.ugen.forced_attach.vendor = 65535
hw.ugen.forced_attach.product = 65535
hw.ugen.forced_attach.addr = -1
hw.ugen.forced_attach.add = 0
hw.ugen.forced_attach.delete = 0
hw.ugen.forced_attach.clear = 0
hw.ugen.forced_attach.list = [0x1050 0x0114 -1]
デバイス削除
$ sudo sysctl -w hw.ugen.forced_attach.vendor=0x1050
hw.ugen.forced_attach.vendor: 65535 -> 4176
$ sudo sysctl -w hw.ugen.forced_attach.product=0x114
hw.ugen.forced_attach.product: 65535 -> 276
$ sudo sysctl -w hw.ugen.forced_attach.delete=1
hw.ugen.forced_attach.delete: 0 -> 1
デバイス全削除
$ sudo sysctl -w hw.ugen.forced_attach.clear=1
hw.ugen.forced_attach.clear: 0 -> 1

課題など

USB スタックにも設定がおかしな USB デバイスが接続された場合の振る舞いを変更するための quirk を決定する処理が実は既に存在しています。

Cross Reference: /src/sys/dev/usb/usb_quirks.c

何故これを使わなかったのかと言えば、動的にデバイスの追加・削除をするのが面倒だったのと Vendor ID と Product ID 以外でも指定したかったからです。現在のパッチでは Vendor ID と Product ID の他に addr が指定できます。これは同じ Vendor ID と Product ID を持っているけれど特定のデバイスだけ ugen(4) として使いたいという要求があったからです。

後で usbdevs -v の出力を見て気付いたのですが、addr だけでは controller だか bus が違う場合にも適用されてしまうのでもう細かく少し指定できる必要がありそうです。それを修正するのは後の課題としたいと思います。

パッチ

GitHubnetbsd-src リポジトリの nonakap-catfood ブランチでパッチは保守しているのでそちらを参照して貰った方が良いかもしれません。

nonakap/netbsd-src at nonakap-catfood · GitHub

commit 9a26f1a4223479ebaa6ea46bb19812036fbe1bd8
Author: NONAKA Kimihiro <nonakap@gmail.com>
Date:   Tue Dec 15 23:52:12 2015 +0900

    ugen(4), sysctl(8): Add forced attach support.

diff --git a/sbin/sysctl/sysctl.c b/sbin/sysctl/sysctl.c
index 622d7fb..6174939 100644
--- a/sbin/sysctl/sysctl.c
+++ b/sbin/sysctl/sysctl.c
@@ -168,6 +168,7 @@ static void kern_cp_id(HANDLER_PROTO);
 static void kern_drivers(HANDLER_PROTO);
 static void vm_loadavg(HANDLER_PROTO);
 static void proc_limit(HANDLER_PROTO);
+static void ugen_forced_attach(HANDLER_PROTO);
 #ifdef CPU_DISKINFO
 static void machdep_diskinfo(HANDLER_PROTO);
 #endif /* CPU_DISKINFO */
@@ -227,6 +228,7 @@ static const struct handlespec {
    { "/net/ns/spp/deb.*",            printother, NULL, "trsp" },
 
    { "/hw/diskstats",            printother, NULL, "iostat" },
+   { "/hw/ugen/forced_attach/list",  ugen_forced_attach, NULL, NULL },
 
 #ifdef CPU_CONSDEV
    { "/machdep/consdev",         kern_consdev, NULL, NULL },
@@ -2558,6 +2560,56 @@ proc_limit(HANDLER_ARGS)
    }
 }
 
+/*ARGSUSED*/
+static void
+ugen_forced_attach(HANDLER_ARGS)
+{
+   struct ugen_forced_attach {
+       struct {
+           uint16_t vendor;
+           uint16_t product;
+       } dev;
+       int addr;
+   } *ufa;
+   size_t sz, i;
+   int rc;
+   const char *comma;
+
+   rc = prog_sysctl(name, namelen, NULL, &sz, NULL, 0);
+   if (rc == -1) {
+       sysctlerror(1);
+       return;
+   }
+
+   if (sz % sizeof(*ufa))
+       err(EXIT_FAILURE,
+           "bad size %zu for hw.ugen.forced_attach.list", sz);
+
+   ufa = malloc(sz);
+   if (ufa == NULL) {
+       sysctlerror(1);
+       return;
+   }
+
+   rc = prog_sysctl(name, namelen, ufa, &sz, NULL, 0);
+   if (rc == -1) {
+       sysctlerror(1);
+       free(ufa);
+       return;
+   }
+
+   comma = "";
+   if (!nflag)
+       printf("%s%s", sname, eq);
+   for (i = 0, sz /= sizeof(*ufa); i < sz; i++) {
+       (void)printf("%s[0x%04x 0x%04x %d]", comma, ufa[i].dev.vendor,
+           ufa[i].dev.product, ufa[i].addr);
+       comma = ", ";
+   }
+   (void)printf("\n");
+   free(ufa);
+}
+
 #ifdef CPU_DISKINFO
 /*ARGSUSED*/
 static void
diff --git a/sys/dev/usb/ugen.c b/sys/dev/usb/ugen.c
index 86cdfa0..5fceb39 100644
--- a/sys/dev/usb/ugen.c
+++ b/sys/dev/usb/ugen.c
@@ -57,10 +57,13 @@ __KERNEL_RCSID(0, "$NetBSD: ugen.c,v 1.126 2014/09/20 08:45:23 gson Exp $");
 #include <sys/proc.h>
 #include <sys/vnode.h>
 #include <sys/poll.h>
+#include <sys/sysctl.h>
+#include <sys/mutex.h>
 
 #include <dev/usb/usb.h>
 #include <dev/usb/usbdi.h>
 #include <dev/usb/usbdi_util.h>
+#include <dev/usb/usbdivar.h>
 
 #ifdef UGEN_DEBUG
 #define DPRINTF(x) if (ugendebug) printf x
@@ -190,20 +193,58 @@ CFATTACH_DECL_NEW(ugen, sizeof(struct ugen_softc), ugen_match, ugen_attach, ugen
 /* toggle to control attach priority. -1 means "let autoconf decide" */
 int ugen_override = -1;
 
+/* forced attach ugen device */
+struct ugen_forced_attach {
+   struct usb_devno dev;
+#define    USB_VENDOR_ANY  0xffff
+   int addr;
+};
+static struct ugen_forced_attach *ugen_forced_attach_tbl;
+static int ugen_forced_attach_num;
+static kmutex_t ugen_forced_attach_mtx;
+static int ugen_forced_attach_vendor = USB_VENDOR_ANY;
+static int ugen_forced_attach_product = USB_PRODUCT_ANY;
+static int ugen_forced_attach_addr = -1;
+
 int
 ugen_match(device_t parent, cfdata_t match, void *aux)
 {
    struct usb_attach_arg *uaa = aux;
-  int override;
+   struct ugen_forced_attach *ufa;
+   int override, i;
 
    if (ugen_override != -1)
        override = ugen_override;
    else
        override = match->cf_flags & 1;
-
    if (override)
        return (UMATCH_HIGHEST);
-  else if (uaa->usegeneric)
+
+   mutex_enter(&ugen_forced_attach_mtx);
+   if (ugen_forced_attach_tbl != NULL) {
+       for (i = 0; i < ugen_forced_attach_num; i++) {
+           ufa = &ugen_forced_attach_tbl[i];
+           if (ufa->dev.ud_vendor == USB_VENDOR_ANY &&
+               ufa->dev.ud_product == USB_PRODUCT_ANY &&
+               ufa->addr == -1)
+               continue;
+
+           if ((ufa->dev.ud_vendor == USB_VENDOR_ANY ||
+                ufa->dev.ud_vendor == uaa->vendor) &&
+               (ufa->dev.ud_product == USB_PRODUCT_ANY ||
+                ufa->dev.ud_product == uaa->product) &&
+               (ufa->addr == -1 ||
+                ufa->addr == uaa->device->address))
+               break;
+       }
+       if (i < ugen_forced_attach_num) {
+           mutex_exit(&ugen_forced_attach_mtx);
+           return (UMATCH_HIGHEST);
+       }
+   }
+   mutex_exit(&ugen_forced_attach_mtx);
+
+   if (uaa->usegeneric)
        return (UMATCH_GENERIC);
    else
        return (UMATCH_NONE);
@@ -2121,3 +2162,247 @@ ugenkqfilter(dev_t dev, struct knote *kn)
 
    return (0);
 }
+
+/*
+ * hw.ugen sysctl
+ */
+static int ugen_forced_attach_operation(SYSCTLFN_ARGS);
+static int ugen_forced_attach_list(SYSCTLFN_ARGS);
+
+SYSCTL_SETUP(sysctl_hw_ugen_setup, "sysctl hw.ugen setup")
+{
+   int err;
+   const struct sysctlnode *rnode;
+   const struct sysctlnode *tnode;
+   const struct sysctlnode *cnode;
+
+   err = sysctl_createv(clog, 0, NULL, &rnode,
+       CTLFLAG_PERMANENT, CTLTYPE_NODE, "ugen",
+       SYSCTL_DESCR("ugen global controls"),
+       NULL, 0, NULL, 0, CTL_HW, CTL_CREATE, CTL_EOL);
+   if (err)
+       goto fail;
+
+   /* override control */
+   err = sysctl_createv(clog, 0, &rnode, &cnode,
+       CTLFLAG_PERMANENT|CTLFLAG_READWRITE, CTLTYPE_INT,
+       "override", SYSCTL_DESCR("override control"),
+       NULL, 0,
+       &ugen_override, sizeof(ugen_override), CTL_CREATE, CTL_EOL);
+   if (err)
+       goto fail;
+
+   /* forced attach control */
+   mutex_init(&ugen_forced_attach_mtx, MUTEX_DEFAULT, IPL_NONE);
+
+   err = sysctl_createv(clog, 0, &rnode, &tnode,
+       CTLFLAG_PERMANENT, CTLTYPE_NODE,
+       "forced_attach", SYSCTL_DESCR("forced attach controls"),
+       NULL, 0, NULL, 0, CTL_CREATE, CTL_EOL);
+   if (err)
+       goto fail;
+   err = sysctl_createv(clog, 0, &tnode, &cnode,
+       CTLFLAG_PERMANENT|CTLFLAG_READWRITE, CTLTYPE_INT,
+       "vendor", SYSCTL_DESCR("vendor"),
+       NULL, 0,
+       &ugen_forced_attach_vendor, sizeof(ugen_forced_attach_vendor),
+       CTL_CREATE, CTL_EOL);
+   if (err)
+       goto fail;
+   err = sysctl_createv(clog, 0, &tnode, &cnode,
+       CTLFLAG_PERMANENT|CTLFLAG_READWRITE, CTLTYPE_INT,
+       "product", SYSCTL_DESCR("product"),
+       NULL, 0,
+       &ugen_forced_attach_product, sizeof(ugen_forced_attach_product),
+       CTL_CREATE, CTL_EOL);
+   if (err)
+       goto fail;
+   err = sysctl_createv(clog, 0, &tnode, &cnode,
+       CTLFLAG_PERMANENT|CTLFLAG_READWRITE, CTLTYPE_INT,
+       "addr", SYSCTL_DESCR("addr"),
+       NULL, 0,
+       &ugen_forced_attach_addr, sizeof(ugen_forced_attach_addr),
+       CTL_CREATE, CTL_EOL);
+   if (err)
+       goto fail;
+   err = sysctl_createv(clog, 0, &tnode, &cnode,
+       CTLFLAG_PERMANENT|CTLFLAG_READWRITE, CTLTYPE_INT,
+       "add", SYSCTL_DESCR("Add a forced attach entry"),
+       ugen_forced_attach_operation, 0, (void *)1, 0, CTL_CREATE, CTL_EOL);
+   if (err)
+       goto fail;
+   err = sysctl_createv(clog, 0, &tnode, &cnode,
+       CTLFLAG_PERMANENT|CTLFLAG_READWRITE, CTLTYPE_INT,
+       "delete", SYSCTL_DESCR("Delete a forced attach entry"),
+       ugen_forced_attach_operation, 0, (void *)2, 0, CTL_CREATE, CTL_EOL);
+   if (err)
+       goto fail;
+   err = sysctl_createv(clog, 0, &tnode, &cnode,
+       CTLFLAG_PERMANENT|CTLFLAG_READWRITE, CTLTYPE_INT,
+       "clear", SYSCTL_DESCR("Clear a forced attach entry"),
+       ugen_forced_attach_operation, 0, (void *)3, 0, CTL_CREATE, CTL_EOL);
+   if (err)
+       goto fail;
+   err = sysctl_createv(clog, 0, &tnode, &cnode,
+       CTLFLAG_PERMANENT|CTLFLAG_READWRITE, CTLTYPE_STRUCT,
+       "list", SYSCTL_DESCR("Get all forced attach entries"),
+       ugen_forced_attach_list, 0, NULL, 0, CTL_CREATE, CTL_EOL);
+   if (err)
+       goto fail;
+
+   return;
+fail:
+   aprint_error("%s: sysctl_createv failed (err = %d)\n", __func__, err);
+}
+
+static int
+ugen_forced_attach_operation(SYSCTLFN_ARGS)
+{
+   struct sysctlnode node;
+   struct ugen_forced_attach *ufa;
+   long op;
+   int i, t, error;
+   bool do_check = true;
+
+   node = *rnode;
+   op = (long)node.sysctl_data;
+
+   t = 0;
+   node.sysctl_data = &t;
+   error = sysctl_lookup(SYSCTLFN_CALL(&node));
+   if (error || newp == NULL)
+       return (error);
+
+   if (t == 0)
+       return (0);
+
+   switch (op) {
+   case 1: /* add */
+   case 2: /* delete */
+       break;
+
+   case 3: /* clear */
+       do_check = false;
+       break;
+
+   default:
+       return (EINVAL);
+   }
+
+   error = EINVAL;
+   mutex_enter(&ugen_forced_attach_mtx);
+
+   if (do_check) {
+       if (ugen_forced_attach_vendor <= 0 ||
+            ugen_forced_attach_vendor > 0xffff)
+           goto out;
+       if (ugen_forced_attach_product <= 0 ||
+            ugen_forced_attach_product > 0xffff)
+           goto out;
+       if (ugen_forced_attach_addr != -1 &&
+           (ugen_forced_attach_addr < 0 ||
+            ugen_forced_attach_addr > 0xff))
+           goto out;
+       if (ugen_forced_attach_vendor == USB_VENDOR_ANY &&
+           ugen_forced_attach_product == USB_PRODUCT_ANY &&
+           ugen_forced_attach_addr == -1)
+           goto out;
+   }
+
+   ufa = NULL;
+   if (do_check && ugen_forced_attach_tbl != NULL) {
+       for (i = 0; i < ugen_forced_attach_num; i++) {
+           ufa = &ugen_forced_attach_tbl[i];
+           if (ufa->dev.ud_vendor == ugen_forced_attach_vendor &&
+               ufa->dev.ud_product == ugen_forced_attach_product &&
+               ufa->addr == ugen_forced_attach_addr)
+               break;
+       }
+       if (i == ugen_forced_attach_num)
+           ufa = NULL;
+   }
+   if (op == 1 && ufa == NULL) {
+       /* Grow the table */
+       ufa = kmem_alloc(sizeof(*ufa) * (ugen_forced_attach_num + 1),
+           KM_NOSLEEP);
+       if (ufa == NULL) {
+           error = ENOMEM;
+           goto out;
+       }
+
+       if (ugen_forced_attach_tbl != NULL)
+           memcpy(ufa, ugen_forced_attach_tbl,
+               sizeof(*ufa) * ugen_forced_attach_num);
+       ugen_forced_attach_tbl = ufa;
+
+       ufa = &ugen_forced_attach_tbl[ugen_forced_attach_num];
+       ufa->dev.ud_vendor = ugen_forced_attach_vendor;
+       ufa->dev.ud_product = ugen_forced_attach_product;
+       ufa->addr = ugen_forced_attach_addr;
+       ugen_forced_attach_num++;
+   } else if (op == 2 && ufa != NULL) {
+       /* Wipe an entry. no shrink the table. */
+       ufa->dev.ud_vendor = USB_VENDOR_ANY;
+       ufa->dev.ud_product = USB_PRODUCT_ANY;
+       ufa->addr = -1;
+   } else if (op == 3 && ugen_forced_attach_tbl != NULL) {
+       /* Drop the table. */
+       kmem_free(ugen_forced_attach_tbl,
+           sizeof(*ufa) * ugen_forced_attach_num);
+       ugen_forced_attach_tbl = NULL;
+       ugen_forced_attach_num = 0;
+   }
+
+   /* Reset parameters */
+   ugen_forced_attach_vendor = USB_VENDOR_ANY;
+   ugen_forced_attach_product = USB_PRODUCT_ANY;
+   ugen_forced_attach_addr = -1;
+
+   error = 0;
+ out:
+   mutex_exit(&ugen_forced_attach_mtx);
+
+   return (error);
+}
+
+static int
+ugen_forced_attach_list(SYSCTLFN_ARGS)
+{
+   struct ugen_forced_attach *ufa;
+   size_t buflen;
+   int error;
+
+   if (oldp == NULL) {
+       mutex_enter(&ugen_forced_attach_mtx);
+       *oldlenp = sizeof(*ufa) * ugen_forced_attach_num;
+       mutex_exit(&ugen_forced_attach_mtx);
+       return (0);
+   }
+   if (*oldlenp == 0)
+       return (0);
+
+   ufa = kmem_alloc(*oldlenp, KM_SLEEP);
+   if (ufa == NULL)
+       return (ENOMEM);
+
+   error = 0;
+   mutex_enter(&ugen_forced_attach_mtx);
+
+   buflen = sizeof(*ufa) * ugen_forced_attach_num;
+   if (*oldlenp < buflen) {
+       error = ENOMEM;
+       goto out;
+   }
+   if (ugen_forced_attach_tbl != NULL)
+       memcpy(ufa, ugen_forced_attach_tbl, buflen);
+
+ out:
+   mutex_exit(&ugen_forced_attach_mtx);
+
+   if (error == 0 && buflen > 0)
+       error = sysctl_copyout(l, ufa, oldp, buflen);
+   kmem_free(ufa, *oldlenp);
+   *oldlenp = buflen;
+
+   return (error);
+}