
2026/03/02 18:26
**Linuxにおけるハードウェア・ホットプラグイベント ― 詳細解説**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
概要:
Libusb の Linux ホットプラグシステムは、
linux_netlink.c と linux_udev.c という 2 つのバックエンドに依存しています。デフォルトでは --with-udev=yes が設定されており、udev を無効にするとプレーンな netlink バックエンドが使用されます。カーネルデバイスイベントは Netlink プロトコル 15(
NETLINK_KOBJECT_UEVENT)を介して到達し、ヌル終端文字列として add@/devices/... のようなアクション行から始まり、ACTION=add、SUBSYSTEM=usb などのキー/バリュー ペアが続きます。udev はこれらのメッセージを受信し解析して、カスタムパケット形式でマルチキャストグループ 2(MONITOR_GROUP_UDEV)に再送信します。udev パケットは
"libudev" というマジック文字列から始まり、ビッグエンディアンのバージョンワード 0xfeedcafe を持ち、次にネイティブエンディアンで格納された複数フィールド(header_sz、properties_off、properties_len、subsystem_hash、devtype_hash、tag_bloom_hi、tag_bloom_lo)が続きます。ハッシュは SUBSYSTEM= と DEVTYPE= の値に対して MurmurHash2 を用いて計算され、2 つの Bloom フィルタワードは TAGS= キーから導出されたビットをエンコードします。その後パケットには元のキー/バリュー文字列と、SO_PASSCRED 経由で送られる Unix 認証情報(pid/uid/gid)が含まれます。カーネルメッセージはゼロ認証情報を持つため、libudev は有効な認証情報がないパケットを拒否します。プロトコルバージョンは固定で
0xfeedcafe となっており、後方互換性や前方互換性に関する保証は文書化されていません。そのため、パケットレイアウト、フィルタリングロジック、または認証情報処理の変更は libusb と udev の両方で協調して更新を行う必要があり、ホットプラグイベントに依存するアプリケーションのデバイス検出、安定性、セキュリティに影響を与える可能性があります。本文
TL;DR – “ここへ行く”
なぜ libusb でホットプラグサポートが必要なのか?
Linux 上では、USB デバイスの挿入・取り外しを検知するために libusb が 2 つのバックエンドを提供しています。
| バックエンド | 使用しているもの |
|---|---|
| udev | + systemd(現在はデフォルト) |
| netlink | カーネルの raw netlink ソケット () |
ホットプラグサポートを追加した最初のコミットでは、選択理由が説明されています。
「Linux バックエンドにホットプラグサポートを追加します。
Linux 用には udev と netlink の 2 通りの構成方法があります。
udev を利用しているシステムでは udev サポートを使用することが強く推奨されます。
この推奨に従い、configure 時にがデフォルトになります。--with-udev=yes
netlink サポートを有効にしたい場合はを指定してください。--with-udev=no
udev サポートが有効になっていると、すべてのデバイス列挙は udev によって行われます。」
libusb が udev を好む理由は、競合状態を回避できる点にあります。udev はデバイスイベントを処理する際に権限変更やファームウェアアップロード、モードスイッチングなどを実施します。カーネルに直接リッスンすると、これらの変更が見逃されてしまう可能性があります。
1. Netlink – 基本的な IPC
Netlink は Linux 固有の「ネットワークプロトコル」であり、カーネルとユーザースペース(および
NETLINK_KOBJECT_UEVENT を使えばユーザースペース同士)間で通信するために使用されます。UDP に似た挙動をし、BSD ソケット上でデータグラムの送受信が可能です。
主な特徴:
- マルチキャスト – 複数のプログラムが同じグループをリッスンできます。
- 付随データ – 例:Unix 認証情報 (
)。SO_PASSCRED - プロトコル ID
(値 15)。NETLINK_KOBJECT_UEVENT
2. サンプルプログラム
以下は、カーネルまたは udev のイベントをリッスンするための簡潔な自己完結型サンプルです。
バッファ拡張や認証チェックは省略しています。
/* コンパイル例: gcc -Wall -O2 -o netlink_example netlink_example.c */ #define _GNU_SOURCE #include <ctype.h> #include <stdio.h> #include <stdbool.h> #include <stdint.h> #include <stdlib.h> #include <string.h> #include <poll.h> #include <arpa/inet.h> #include <sys/socket.h> /* Netlink 定数 – Linux ヘッダは不要 */ #define NETLINK_KOBJECT_UEVENT 15 #define MONITOR_GROUP_KERNEL 1 #define MONITOR_GROUP_UDEV 2 /* sockaddr_nl の最小定義 */ struct sockaddr_nl { sa_family_t nl_family; unsigned short nl_pad; uint32_t nl_pid; uint32_t nl_groups; }; /* ---------- ヘルパー関数 --------------------------------------- */ void print_kern_uevent_pkt(void *buf, size_t bufsz) { while (bufsz) { int this_sz = printf("%s\n", (char *)buf); buf += this_sz; bufsz -= this_sz; } } struct udev_packet_header { char libudev_magic[8]; uint32_t magic; /* 0xfeedcafe, ビッグエンディアン */ uint32_t header_sz; /* ネイティブエンディアン */ uint32_t properties_off; /* ネイティブエンディアン */ uint32_t properties_len; /* ネイティブエンディアン */ uint32_t subsystem_hash; uint32_t devtype_hash; uint32_t tag_bloom_hi; uint32_t tag_bloom_lo; }; void print_udev_pkt(void *buf, size_t bufsz) { struct udev_packet_header hdr; if (bufsz < sizeof(hdr)) { printf("Invalid packet!\n"); return; } memcpy(&hdr, buf, sizeof(hdr)); /* ビッグエンディアンフィールドを変換 */ hdr.magic = ntohl(hdr.magic); hdr.subsystem_hash = ntohl(hdr.subsystem_hash); hdr.devtype_hash = ntohl(hdr.devtype_hash); hdr.tag_bloom_hi = ntohl(hdr.tag_bloom_hi); hdr.tag_bloom_lo = ntohl(hdr.tag_bloom_lo); if (memcmp(hdr.libudev_magic, "libudev", 8) || hdr.magic != 0xfeedcafe) { printf("Invalid packet magic!\n"); return; } /* ペイロード文字列を表示 */ print_kern_uevent_pkt((char *)buf + hdr.properties_off, hdr.properties_len); } /* --------------------------------------------------------------------- */ int main(int argc, char **argv) { if (argc < 2) { printf("Usage: %s kernel|udev\n", argv[0]); return 1; } bool udev_mode = !strcmp(argv[1], "udev"); /* Netlink ソケットをオープン */ int nlsock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT); if (nlsock == -1) { perror("socket"); return 1; } /* 必要なマルチキャストグループにバインド */ struct sockaddr_nl sa_nl; memset(&sa_nl, 0, sizeof(sa_nl)); sa_nl.nl_family = AF_NETLINK; sa_nl.nl_groups = udev_mode ? MONITOR_GROUP_UDEV : MONITOR_GROUP_KERNEL; if (bind(nlsock, (struct sockaddr *)&sa_nl, sizeof(sa_nl)) == -1) { perror("bind"); return 1; } /* 受信ループ */ char *buf = NULL; size_t buf_sz = 0; while (true) { ssize_t pkt_sz = recv(nlsock, NULL, 0, MSG_PEEK | MSG_TRUNC); if (pkt_sz == -1) { perror("peek"); return 1; } if ((size_t)pkt_sz > buf_sz) { buf = realloc(buf, pkt_sz); buf_sz = pkt_sz; } /* パケットを読み込む */ struct iovec iov = {.iov_base = buf, .iov_len = pkt_sz}; struct msghdr msg = {.msg_iov = &iov, .msg_iovlen = 1}; if (recvmsg(nlsock, &msg, 0) != pkt_sz) { perror("recv"); return 1; } /* ディスパッチ */ printf("\n--- %s event ---\n", udev_mode ? "udev" : "kernel"); if (udev_mode) print_udev_pkt(buf, pkt_sz); else print_kern_uevent_pkt(buf, pkt_sz); } }
-
カーネルイベント – マルチキャストグループ
。1
ペイロードは NUL 終端文字列の連続 (
,ACTION=add
など)。SUBSYSTEM=usb -
udev 再送信イベント – マルチキャストグループ
。2
ペイロードは先頭に 8 バイト
のマジックが入り、バイナリヘッダと同じ文字列リストが続きます。"libudev"
3. udev パケットの構造
00000000: 6c 69 62 75 64 65 76 00 fe ed ca fe 28 00 00 00 ...
- ヘッダ – 32 バイト(
,magic
等以外はリトルエンディアン)。subsystem_hash - ペイロード – NUL 終端文字列で、カーネルパケットと同一。
ヘッダに含まれるフィールド:
| フィールド | 意味 |
|---|---|
| + NUL |
| 0xfeedcafe(ビッグエンディアン) |
| ヘッダサイズ(ネイティブエンディアン) |
, | 文字列ブロックのオフセットと長さ |
| の MurmurHash2 値 |
| の MurmurHash2 値 |
| エントリの 64 ビット Bloom フィルタ |
これらのハッシュにより、BPF プログラムはカーネルがプロセスを起動する前にメッセージを事前フィルタリングできます。
4. セキュリティ
- 認証情報 – udev は
を介して PID, UID, GID を送信します。SO_PASSCRED
libudev はこれらが無いパケットを拒否し、カーネルパケットはすべてゼロになります。 - 許可された送信者 – 通常は root(またはユーザー名前空間内のプロセス)だけが udev マルチキャストグループに送信できます。
5. 要点まとめ
| 条件 | 推奨バックエンド |
|---|---|
を利用しているシステムで確実な USB ホットプラグ検知が必要 | udev () |
| カーネルイベントを直接受け取りたい | ソケット、プロトコル を使用し、グループ 1 にバインド |
| udev の再送信イベントを受信したい | グループ 2 にバインドし、上記のバイナリヘッダを解析 |
以上が libusb が採用しているホットプラグバックエンドと、それに関わる IPC メカニズムの概要です。