From 8fc87a729707b5ae3f67b157f683d0807667a8a8 Mon Sep 17 00:00:00 2001 From: falkTX Date: Fri, 24 Apr 2026 12:26:46 +0200 Subject: [PATCH 1/3] Dynamically load libudev in linux-hidraw backend Signed-off-by: falkTX --- linux/CMakeLists.txt | 5 +- linux/hid.c | 4 +- linux/libudev.c | 124 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 linux/libudev.c diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 9c627087f..8f83ee47e 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -8,10 +8,7 @@ target_link_libraries(hidapi_hidraw PUBLIC hidapi_include) find_package(Threads REQUIRED) -include(FindPkgConfig) -pkg_check_modules(libudev REQUIRED IMPORTED_TARGET libudev) - -target_link_libraries(hidapi_hidraw PRIVATE PkgConfig::libudev Threads::Threads) +target_link_libraries(hidapi_hidraw PRIVATE Threads::Threads dl) set_target_properties(hidapi_hidraw PROPERTIES diff --git a/linux/hid.c b/linux/hid.c index a4dc26f4e..fd199f68b 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -40,7 +40,9 @@ #include #include #include -#include + +/* falkTX: replaced by local file for dynamic loading */ +#include "libudev.c" #include "hidapi.h" diff --git a/linux/libudev.c b/linux/libudev.c new file mode 100644 index 000000000..0b2999813 --- /dev/null +++ b/linux/libudev.c @@ -0,0 +1,124 @@ +/* SPDX-FileCopyrightText: 2026 Filipe Coelho */ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include +#include + +#define C(FN) \ + static typeof(FN)* FN; \ + if (FN == NULL) { \ + void *realfn = dlsym(NULL, #FN); \ + FN = *(typeof(FN)*)&realfn; \ + } + +#define udev_list_entry_foreach(varname, front) \ + for (varname = front; varname != NULL; varname = udev_list_entry_get_next(varname)) + +struct udev *udev_new(void) +{ + static void* _lib; + if (_lib == NULL) + { + _lib = dlopen("libudev.so.1", RTLD_NOW | RTLD_GLOBAL); + if (_lib == NULL) + return NULL; + } + + C(udev_new); + return udev_new(); +} + +struct udev *udev_unref(struct udev *udev) +{ + C(udev_unref); + return udev_unref(udev); +} + +struct udev_device *udev_device_new_from_devnum(struct udev *udev, char type, dev_t devnum) +{ + C(udev_device_new_from_devnum); + return udev_device_new_from_devnum(udev, type, devnum); +} + +struct udev_device *udev_device_new_from_syspath(struct udev *udev, const char *syspath) +{ + C(udev_device_new_from_syspath); + return udev_device_new_from_syspath(udev, syspath); +} + +struct udev_device *udev_device_unref(struct udev_device *udev_device) +{ + C(udev_device_unref); + return udev_device_unref(udev_device); +} + +struct udev_device *udev_device_get_parent_with_subsystem_devtype(struct udev_device *udev_device, + const char *subsystem, const char *devtype) +{ + C(udev_device_get_parent_with_subsystem_devtype); + return udev_device_get_parent_with_subsystem_devtype(udev_device, subsystem, devtype); +} + +const char* udev_device_get_syspath(struct udev_device *udev_device) +{ + C(udev_device_get_syspath); + return udev_device_get_syspath(udev_device); +} + +const char* udev_device_get_devnode(struct udev_device *udev_device) +{ + C(udev_device_get_devnode); + return udev_device_get_devnode(udev_device); +} + +const char* udev_device_get_sysattr_value(struct udev_device *udev_device, const char *sysattr) +{ + C(udev_device_get_sysattr_value); + return udev_device_get_sysattr_value(udev_device, sysattr); +} + +struct udev_enumerate *udev_enumerate_new(struct udev *udev) +{ + C(udev_enumerate_new); + return udev_enumerate_new(udev); +} + +struct udev_enumerate *udev_enumerate_unref(struct udev_enumerate *udev_enumerate) +{ + C(udev_enumerate_unref); + return udev_enumerate_unref(udev_enumerate); +} + +int udev_enumerate_add_match_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem) +{ + C(udev_enumerate_add_match_subsystem); + return udev_enumerate_add_match_subsystem(udev_enumerate, subsystem); +} + +int udev_enumerate_scan_devices(struct udev_enumerate *udev_enumerate) +{ + C(udev_enumerate_scan_devices); + return udev_enumerate_scan_devices(udev_enumerate); +} + +struct udev_list_entry *udev_enumerate_get_list_entry(struct udev_enumerate *udev_enumerate) +{ + C(udev_enumerate_get_list_entry); + return udev_enumerate_get_list_entry(udev_enumerate); +} + +struct udev_list_entry *udev_list_entry_get_next(struct udev_list_entry *list_entry) +{ + C(udev_list_entry_get_next); + return udev_list_entry_get_next(list_entry); +} + +const char* udev_list_entry_get_name(struct udev_list_entry *list_entry) +{ + C(udev_list_entry_get_name); + return udev_list_entry_get_name(list_entry); +} + +#undef C From a181a2ff7a2b0e47561fd33b67801dd7ce50a86f Mon Sep 17 00:00:00 2001 From: Ihor Dutchak Date: Thu, 30 Jul 2026 17:56:10 +0300 Subject: [PATCH 2/3] Address dynamic libudev review feedback Load libudev.so.1 through a scoped runtime handle, resolve the required entry points, and clean up partial and successful initialization state. Remove the build-time libudev development dependency and cover reload, failure recovery, and balanced unload behavior in Linux CI. Assisted-by: codex:gpt-5.6-sol --- .cirrus.yml | 2 +- .github/workflows/builds.yml | 24 +++- .github/workflows/checks.yml | 2 +- BUILD.md | 10 +- CMakeLists.txt | 3 +- configure.ac | 9 +- linux/CMakeLists.txt | 6 +- linux/Makefile-manual | 9 +- linux/Makefile.am | 7 +- linux/hid.c | 145 +++++++++++++++++++++- linux/libudev.c | 124 ------------------- linux/test/CMakeLists.txt | 46 +++++++ linux/test/dlopen_interposer.c | 126 +++++++++++++++++++ linux/test/hidapi_hidraw_loader_test.c | 164 +++++++++++++++++++++++++ linux/test/incomplete_udev.c | 41 +++++++ pc/hidapi-hidraw.pc.in | 1 + src/CMakeLists.txt | 2 - src/cmake/hidapi-config.cmake.in | 10 +- 18 files changed, 566 insertions(+), 165 deletions(-) delete mode 100644 linux/libudev.c create mode 100644 linux/test/CMakeLists.txt create mode 100644 linux/test/dlopen_interposer.c create mode 100644 linux/test/hidapi_hidraw_loader_test.c create mode 100644 linux/test/incomplete_udev.c diff --git a/.cirrus.yml b/.cirrus.yml index b4cf20166..95167de78 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,7 +1,7 @@ alpine_task: container: image: alpine:latest - install_script: apk add autoconf automake g++ gcc libusb-dev libtool linux-headers eudev-dev make musl-dev + install_script: apk add autoconf automake g++ gcc libusb-dev libtool linux-headers eudev-libs make musl-dev script: - ./bootstrap - ./configure || { cat config.log; exit 1; } diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 83ebe190a..5c1c177b0 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -108,19 +108,25 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install libudev-dev libusb-1.0-0-dev python3-pip ninja-build + sudo apt install libudev1 libusb-1.0-0-dev python3-pip ninja-build sudo -H pip3 install meson - name: Configure CMake run: | rm -rf build install - cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}" - cmake -B build/static -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/static -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}" + cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DHIDAPI_WITH_TESTS=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}" + cmake -B build/static -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DHIDAPI_WITH_TESTS=ON -DCMAKE_INSTALL_PREFIX=install/static -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}" - name: Build CMake Shared working-directory: build/shared run: make install + - name: Test CMake Shared + working-directory: build/shared + run: ctest --output-on-failure - name: Build CMake Static working-directory: build/static run: make install + - name: Test CMake Static + working-directory: build/static + run: ctest --output-on-failure - name: Check artifacts uses: andstor/file-existence-action@v2 with: @@ -515,12 +521,12 @@ jobs: path: hidapisrc - name: Install dependencies run: | - apk add gcc musl-dev autoconf automake libtool eudev-dev libusb-dev linux-headers cmake ninja make + apk add gcc musl-dev autoconf automake libtool eudev-libs libusb-dev linux-headers cmake ninja make - name: Configure CMake run: | rm -rf build install - cmake -B build/shared-cmake -S hidapisrc -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install/shared-cmake -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${ALPINE_COMPILE_FLAGS}" - cmake -B build/static-cmake -S hidapisrc -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install/static-cmake -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${ALPINE_COMPILE_FLAGS}" + cmake -B build/shared-cmake -S hidapisrc -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DCMAKE_INSTALL_PREFIX=install/shared-cmake -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${ALPINE_COMPILE_FLAGS}" + cmake -B build/static-cmake -S hidapisrc -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DCMAKE_INSTALL_PREFIX=install/static-cmake -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${ALPINE_COMPILE_FLAGS}" - name: Configure Automake working-directory: hidapisrc run: | @@ -529,9 +535,15 @@ jobs: - name: Build CMake Shared working-directory: build/shared-cmake run: ninja install + - name: Test CMake Shared + working-directory: build/shared-cmake + run: ctest --output-on-failure - name: Build CMake Static working-directory: build/static-cmake run: ninja install + - name: Test CMake Static + working-directory: build/static-cmake + run: ctest --output-on-failure - name: Build Automake working-directory: hidapisrc run: | diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 217853f5a..d3ac0489c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -141,7 +141,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install libudev-dev libusb-1.0-0-dev ninja-build + sudo apt install libudev1 libusb-1.0-0-dev ninja-build - name: Configure run: | cmake -B build -S src -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_HIDTEST=ON -DCMAKE_C_COMPILER=gcc diff --git a/BUILD.md b/BUILD.md index d7a3546f6..1211d61d3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -45,14 +45,14 @@ Regardless of what build system you choose to use, there are specific dependenci ### Linux: Depending on which backend you're going to build, you'll need to install -additional development packages. For `linux/hidraw` backend, you need a -development package for `libudev`. For `libusb` backend, naturally, you need -`libusb` development package. +additional packages. The `linux/hidraw` backend loads `libudev` dynamically, +so it only needs the runtime library. The `libusb` backend needs the `libusb` +development package. On Debian/Ubuntu systems these can be installed by running: ```sh -# required only by hidraw backend -sudo apt install libudev-dev +# required at runtime by the hidraw backend +sudo apt install libudev1 # required only by libusb backend sudo apt install libusb-1.0-0-dev ``` diff --git a/CMakeLists.txt b/CMakeLists.txt index d7086813c..3ab8aff37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,10 +71,9 @@ if(HIDAPI_ENABLE_ASAN) endif() if(WIN32) - # so far only Windows has tests option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" ${IS_DEBUG_BUILD}) else() - set(HIDAPI_WITH_TESTS OFF) + option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" OFF) endif() if(HIDAPI_WITH_TESTS) diff --git a/configure.ac b/configure.ac index 1b2051052..312a2cd51 100644 --- a/configure.ac +++ b/configure.ac @@ -57,9 +57,12 @@ case $host in threads="pthreads" # HIDAPI/hidraw libs - PKG_CHECK_MODULES([libudev], [libudev], true, [hidapi_lib_error libudev]) - LIBS_HIDRAW_PR="${LIBS_HIDRAW_PR} $libudev_LIBS" - CFLAGS_HIDRAW="${CFLAGS_HIDRAW} $libudev_CFLAGS" + hidapi_saved_LIBS="$LIBS" + AC_SEARCH_LIBS([dlopen], [dl], [], [hidapi_lib_error libdl]) + if test "x$ac_cv_search_dlopen" != "xnone required"; then + LIBS_HIDRAW_PR="${LIBS_HIDRAW_PR} ${ac_cv_search_dlopen}" + fi + LIBS="$hidapi_saved_LIBS" # HIDAPI/libusb libs AC_CHECK_LIB([rt], [clock_gettime], [LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} -lrt"], [hidapi_lib_error librt]) diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 8f83ee47e..a1e0e38ae 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -8,7 +8,7 @@ target_link_libraries(hidapi_hidraw PUBLIC hidapi_include) find_package(Threads REQUIRED) -target_link_libraries(hidapi_hidraw PRIVATE Threads::Threads dl) +target_link_libraries(hidapi_hidraw PRIVATE Threads::Threads ${CMAKE_DL_LIBS}) set_target_properties(hidapi_hidraw PROPERTIES @@ -24,6 +24,10 @@ add_library(hidapi::hidraw ALIAS hidapi_hidraw) # compatibility with raw library link add_library(hidapi-hidraw ALIAS hidapi_hidraw) +if(HIDAPI_WITH_TESTS) + add_subdirectory(test) +endif() + if(HIDAPI_INSTALL_TARGETS) install(TARGETS hidapi_hidraw EXPORT hidapi LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" diff --git a/linux/Makefile-manual b/linux/Makefile-manual index 81d28cf2c..357df50c8 100644 --- a/linux/Makefile-manual +++ b/linux/Makefile-manual @@ -18,18 +18,17 @@ LDFLAGS ?= -Wall -g COBJS = hid.o ../hidtest/test.o OBJS = $(COBJS) -LIBS_UDEV = `pkg-config libudev --libs` -lrt -LIBS = $(LIBS_UDEV) -INCLUDES ?= -I../hidapi `pkg-config libusb-1.0 --cflags` +LIBS_HIDRAW = -ldl +INCLUDES ?= -I../hidapi # Console Test Program hidtest-hidraw: $(COBJS) - $(CC) $(LDFLAGS) $^ $(LIBS_UDEV) -o $@ + $(CC) $(LDFLAGS) $^ $(LIBS_HIDRAW) -o $@ # Shared Libs libhidapi-hidraw.so: $(COBJS) - $(CC) $(LDFLAGS) $(LIBS_UDEV) -shared -fpic -Wl,-soname,$@.0 $^ -o $@ + $(CC) $(LDFLAGS) -shared -fpic -Wl,-soname,$@.0 $^ $(LIBS_HIDRAW) -o $@ # Objects $(COBJS): %.o: %.c diff --git a/linux/Makefile.am b/linux/Makefile.am index 230eeb75a..1ef290b69 100644 --- a/linux/Makefile.am +++ b/linux/Makefile.am @@ -7,4 +7,9 @@ libhidapi_hidraw_la_LIBADD = $(LIBS_HIDRAW) hdrdir = $(includedir)/hidapi hdr_HEADERS = $(top_srcdir)/hidapi/hidapi.h -EXTRA_DIST = Makefile-manual +EXTRA_DIST = \ + Makefile-manual \ + test/CMakeLists.txt \ + test/dlopen_interposer.c \ + test/hidapi_hidraw_loader_test.c \ + test/incomplete_udev.c diff --git a/linux/hid.c b/linux/hid.c index fd199f68b..1128f7d9e 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -26,6 +26,7 @@ #include #include #include +#include /* Unix */ #include @@ -35,17 +36,58 @@ #include #include #include +#include /* Linux */ #include #include #include -/* falkTX: replaced by local file for dynamic loading */ -#include "libudev.c" - #include "hidapi.h" +struct udev; +struct udev_device; +struct udev_enumerate; +struct udev_list_entry; + +typedef struct udev *udev_new_(void); +typedef struct udev *udev_unref_(struct udev *udev); +typedef struct udev_device *udev_device_new_from_devnum_(struct udev *udev, char type, dev_t devnum); +typedef struct udev_device *udev_device_new_from_syspath_(struct udev *udev, const char *syspath); +typedef struct udev_device *udev_device_unref_(struct udev_device *udev_device); +typedef struct udev_device *udev_device_get_parent_with_subsystem_devtype_( + struct udev_device *udev_device, const char *subsystem, const char *devtype); +typedef const char *udev_device_get_syspath_(struct udev_device *udev_device); +typedef const char *udev_device_get_devnode_(struct udev_device *udev_device); +typedef const char *udev_device_get_sysattr_value_(struct udev_device *udev_device, const char *sysattr); +typedef struct udev_enumerate *udev_enumerate_new_(struct udev *udev); +typedef struct udev_enumerate *udev_enumerate_unref_(struct udev_enumerate *udev_enumerate); +typedef int udev_enumerate_add_match_subsystem_(struct udev_enumerate *udev_enumerate, const char *subsystem); +typedef int udev_enumerate_scan_devices_(struct udev_enumerate *udev_enumerate); +typedef struct udev_list_entry *udev_enumerate_get_list_entry_(struct udev_enumerate *udev_enumerate); +typedef struct udev_list_entry *udev_list_entry_get_next_(struct udev_list_entry *list_entry); +typedef const char *udev_list_entry_get_name_(struct udev_list_entry *list_entry); + +static udev_new_ *udev_new = NULL; +static udev_unref_ *udev_unref = NULL; +static udev_device_new_from_devnum_ *udev_device_new_from_devnum = NULL; +static udev_device_new_from_syspath_ *udev_device_new_from_syspath = NULL; +static udev_device_unref_ *udev_device_unref = NULL; +static udev_device_get_parent_with_subsystem_devtype_ *udev_device_get_parent_with_subsystem_devtype = NULL; +static udev_device_get_syspath_ *udev_device_get_syspath = NULL; +static udev_device_get_devnode_ *udev_device_get_devnode = NULL; +static udev_device_get_sysattr_value_ *udev_device_get_sysattr_value = NULL; +static udev_enumerate_new_ *udev_enumerate_new = NULL; +static udev_enumerate_unref_ *udev_enumerate_unref = NULL; +static udev_enumerate_add_match_subsystem_ *udev_enumerate_add_match_subsystem = NULL; +static udev_enumerate_scan_devices_ *udev_enumerate_scan_devices = NULL; +static udev_enumerate_get_list_entry_ *udev_enumerate_get_list_entry = NULL; +static udev_list_entry_get_next_ *udev_list_entry_get_next = NULL; +static udev_list_entry_get_name_ *udev_list_entry_get_name = NULL; + +static void *udev_lib_handle = NULL; +static int hidapi_initialized = 0; + #ifdef HIDAPI_ALLOW_BUILD_WORKAROUND_KERNEL_2_6_39 /* This definitions first appeared in Linux Kernel 2.6.39 in linux/hidraw.h. hidapi doesn't support kernels older than that, @@ -168,6 +210,84 @@ static void register_global_error_format(const char *format, ...) va_end(args); } +static void free_udev_library(void) +{ + if (udev_lib_handle) + dlclose(udev_lib_handle); + udev_lib_handle = NULL; + + udev_new = NULL; + udev_unref = NULL; + udev_device_new_from_devnum = NULL; + udev_device_new_from_syspath = NULL; + udev_device_unref = NULL; + udev_device_get_parent_with_subsystem_devtype = NULL; + udev_device_get_syspath = NULL; + udev_device_get_devnode = NULL; + udev_device_get_sysattr_value = NULL; + udev_enumerate_new = NULL; + udev_enumerate_unref = NULL; + udev_enumerate_add_match_subsystem = NULL; + udev_enumerate_scan_devices = NULL; + udev_enumerate_get_list_entry = NULL; + udev_list_entry_get_next = NULL; + udev_list_entry_get_name = NULL; +} + +static int lookup_udev_functions(void) +{ + udev_lib_handle = dlopen("libudev.so.1", RTLD_NOW | RTLD_LOCAL); + if (!udev_lib_handle) { + const char *error = dlerror(); + register_global_error_format("Failed to load libudev.so.1: %s", error ? error : "unknown error"); + return -1; + } + +/* Avoid direct function-pointer casts from void pointers. + Using memcpy keeps this warning-free regardless of the compiler settings. */ +#define HIDAPI_UDEV_RESOLVE(name) do { \ + void *symbol; \ + const char *error; \ + dlerror(); \ + symbol = dlsym(udev_lib_handle, #name); \ + error = dlerror(); \ + if (error) { \ + register_global_error_format("Failed to resolve libudev symbol %s: %s", #name, error); \ + goto err; \ + } \ + if (sizeof(name) != sizeof(symbol)) { \ + register_global_error_format("Failed to resolve libudev symbol %s: incompatible pointer sizes", #name); \ + goto err; \ + } \ + memcpy(&name, &symbol, sizeof(symbol)); \ +} while (0) + + HIDAPI_UDEV_RESOLVE(udev_new); + HIDAPI_UDEV_RESOLVE(udev_unref); + HIDAPI_UDEV_RESOLVE(udev_device_new_from_devnum); + HIDAPI_UDEV_RESOLVE(udev_device_new_from_syspath); + HIDAPI_UDEV_RESOLVE(udev_device_unref); + HIDAPI_UDEV_RESOLVE(udev_device_get_parent_with_subsystem_devtype); + HIDAPI_UDEV_RESOLVE(udev_device_get_syspath); + HIDAPI_UDEV_RESOLVE(udev_device_get_devnode); + HIDAPI_UDEV_RESOLVE(udev_device_get_sysattr_value); + HIDAPI_UDEV_RESOLVE(udev_enumerate_new); + HIDAPI_UDEV_RESOLVE(udev_enumerate_unref); + HIDAPI_UDEV_RESOLVE(udev_enumerate_add_match_subsystem); + HIDAPI_UDEV_RESOLVE(udev_enumerate_scan_devices); + HIDAPI_UDEV_RESOLVE(udev_enumerate_get_list_entry); + HIDAPI_UDEV_RESOLVE(udev_list_entry_get_next); + HIDAPI_UDEV_RESOLVE(udev_list_entry_get_name); + +#undef HIDAPI_UDEV_RESOLVE + + return 0; + +err: + free_udev_library(); + return -1; +} + /* Set the last error for a device to be reported by hid_error(dev). * The given error message will be copied (and decoded according to the * currently locale, so do not pass in string constants). @@ -919,11 +1039,20 @@ int HID_API_EXPORT hid_init(void) if (!locale) setlocale(LC_CTYPE, ""); + if (!hidapi_initialized) { + if (lookup_udev_functions() < 0) + return -1; + hidapi_initialized = 1; + } + return 0; } int HID_API_EXPORT hid_exit(void) { + free_udev_library(); + hidapi_initialized = 0; + /* Free global error message */ register_global_error(NULL); @@ -939,7 +1068,8 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; - hid_init(); + if (hid_init() < 0) + return NULL; /* register_global_error: global error is reset by hid_init */ /* Create the udev object */ @@ -956,7 +1086,9 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, devices = udev_enumerate_get_list_entry(enumerate); /* For each item, see if it matches the vid/pid, and if so create a udev_device record for it */ - udev_list_entry_foreach(dev_list_entry, devices) { + for (dev_list_entry = devices; + dev_list_entry != NULL; + dev_list_entry = udev_list_entry_get_next(dev_list_entry)) { const char *sysfs_path; unsigned short dev_vid = 0; unsigned short dev_pid = 0; @@ -1078,7 +1210,8 @@ hid_device * HID_API_EXPORT hid_open_path(const char *path) { hid_device *dev = NULL; - hid_init(); + if (hid_init() < 0) + return NULL; /* register_global_error: global error is reset by hid_init */ dev = new_hid_device(); diff --git a/linux/libudev.c b/linux/libudev.c deleted file mode 100644 index 0b2999813..000000000 --- a/linux/libudev.c +++ /dev/null @@ -1,124 +0,0 @@ -/* SPDX-FileCopyrightText: 2026 Filipe Coelho */ -/* SPDX-License-Identifier: LGPL-2.1-or-later */ - -#include -#include -#include -#include - -#define C(FN) \ - static typeof(FN)* FN; \ - if (FN == NULL) { \ - void *realfn = dlsym(NULL, #FN); \ - FN = *(typeof(FN)*)&realfn; \ - } - -#define udev_list_entry_foreach(varname, front) \ - for (varname = front; varname != NULL; varname = udev_list_entry_get_next(varname)) - -struct udev *udev_new(void) -{ - static void* _lib; - if (_lib == NULL) - { - _lib = dlopen("libudev.so.1", RTLD_NOW | RTLD_GLOBAL); - if (_lib == NULL) - return NULL; - } - - C(udev_new); - return udev_new(); -} - -struct udev *udev_unref(struct udev *udev) -{ - C(udev_unref); - return udev_unref(udev); -} - -struct udev_device *udev_device_new_from_devnum(struct udev *udev, char type, dev_t devnum) -{ - C(udev_device_new_from_devnum); - return udev_device_new_from_devnum(udev, type, devnum); -} - -struct udev_device *udev_device_new_from_syspath(struct udev *udev, const char *syspath) -{ - C(udev_device_new_from_syspath); - return udev_device_new_from_syspath(udev, syspath); -} - -struct udev_device *udev_device_unref(struct udev_device *udev_device) -{ - C(udev_device_unref); - return udev_device_unref(udev_device); -} - -struct udev_device *udev_device_get_parent_with_subsystem_devtype(struct udev_device *udev_device, - const char *subsystem, const char *devtype) -{ - C(udev_device_get_parent_with_subsystem_devtype); - return udev_device_get_parent_with_subsystem_devtype(udev_device, subsystem, devtype); -} - -const char* udev_device_get_syspath(struct udev_device *udev_device) -{ - C(udev_device_get_syspath); - return udev_device_get_syspath(udev_device); -} - -const char* udev_device_get_devnode(struct udev_device *udev_device) -{ - C(udev_device_get_devnode); - return udev_device_get_devnode(udev_device); -} - -const char* udev_device_get_sysattr_value(struct udev_device *udev_device, const char *sysattr) -{ - C(udev_device_get_sysattr_value); - return udev_device_get_sysattr_value(udev_device, sysattr); -} - -struct udev_enumerate *udev_enumerate_new(struct udev *udev) -{ - C(udev_enumerate_new); - return udev_enumerate_new(udev); -} - -struct udev_enumerate *udev_enumerate_unref(struct udev_enumerate *udev_enumerate) -{ - C(udev_enumerate_unref); - return udev_enumerate_unref(udev_enumerate); -} - -int udev_enumerate_add_match_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem) -{ - C(udev_enumerate_add_match_subsystem); - return udev_enumerate_add_match_subsystem(udev_enumerate, subsystem); -} - -int udev_enumerate_scan_devices(struct udev_enumerate *udev_enumerate) -{ - C(udev_enumerate_scan_devices); - return udev_enumerate_scan_devices(udev_enumerate); -} - -struct udev_list_entry *udev_enumerate_get_list_entry(struct udev_enumerate *udev_enumerate) -{ - C(udev_enumerate_get_list_entry); - return udev_enumerate_get_list_entry(udev_enumerate); -} - -struct udev_list_entry *udev_list_entry_get_next(struct udev_list_entry *list_entry) -{ - C(udev_list_entry_get_next); - return udev_list_entry_get_next(list_entry); -} - -const char* udev_list_entry_get_name(struct udev_list_entry *list_entry) -{ - C(udev_list_entry_get_name); - return udev_list_entry_get_name(list_entry); -} - -#undef C diff --git a/linux/test/CMakeLists.txt b/linux/test/CMakeLists.txt new file mode 100644 index 000000000..428b4065d --- /dev/null +++ b/linux/test/CMakeLists.txt @@ -0,0 +1,46 @@ +add_executable(hidapi_hidraw_loader_test + hidapi_hidraw_loader_test.c +) +target_link_libraries(hidapi_hidraw_loader_test PRIVATE hidapi_hidraw) +target_link_libraries(hidapi_hidraw_loader_test PRIVATE ${CMAKE_DL_LIBS}) + +add_library(hidapi_test_incomplete_udev SHARED + incomplete_udev.c +) + +add_library(hidapi_test_dlopen_interposer SHARED + dlopen_interposer.c +) +target_link_libraries(hidapi_test_dlopen_interposer PRIVATE ${CMAKE_DL_LIBS}) + +add_dependencies(hidapi_hidraw_loader_test + hidapi_test_incomplete_udev + hidapi_test_dlopen_interposer +) + +add_test( + NAME hidapi-hidraw-loader-reload + COMMAND + "${CMAKE_COMMAND}" -E env + "ASAN_OPTIONS=verify_asan_link_order=0" + "LD_PRELOAD=$" + "HIDAPI_TEST_FAIL_ATTEMPTS=0" + "$" reload +) + +add_test( + NAME hidapi-hidraw-loader-failure-recovery + COMMAND + "${CMAKE_COMMAND}" -E env + "ASAN_OPTIONS=verify_asan_link_order=0" + "LD_PRELOAD=$" + "HIDAPI_TEST_FAKE_UDEV=$" + "HIDAPI_TEST_FAIL_ATTEMPTS=2" + "$" failure-recovery +) + +set_tests_properties( + hidapi-hidraw-loader-reload + hidapi-hidraw-loader-failure-recovery + PROPERTIES TIMEOUT 30 +) diff --git a/linux/test/dlopen_interposer.c b/linux/test/dlopen_interposer.c new file mode 100644 index 000000000..891e697ab --- /dev/null +++ b/linux/test/dlopen_interposer.c @@ -0,0 +1,126 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#define _GNU_SOURCE + +#include +#include +#include +#include + + +typedef void *dlopen_(const char *filename, int flags); +typedef int dlclose_(void *handle); + +static dlopen_ *real_dlopen; +static dlclose_ *real_dlclose; +static void *udev_handle; +static unsigned int udev_attempts; +static unsigned int udev_open_count; +static unsigned int udev_close_count; + +static int resolve_real_functions(void) +{ + void *symbol; + + if (!real_dlopen) { + symbol = dlsym(RTLD_NEXT, "dlopen"); + if (!symbol || sizeof(real_dlopen) != sizeof(symbol)) + return -1; + memcpy(&real_dlopen, &symbol, sizeof(symbol)); + } + + if (!real_dlclose) { + symbol = dlsym(RTLD_NEXT, "dlclose"); + if (!symbol || sizeof(real_dlclose) != sizeof(symbol)) + return -1; + memcpy(&real_dlclose, &symbol, sizeof(symbol)); + } + + return 0; +} + +static unsigned int failure_attempt_limit(void) +{ + const char *value = getenv("HIDAPI_TEST_FAIL_ATTEMPTS"); + char *end; + unsigned long limit; + + if (!value) + return 0; + + limit = strtoul(value, &end, 10); + if (*value == '\0' || *end != '\0' || limit > UINT_MAX) + return 0; + + return (unsigned int) limit; +} + +void *dlopen(const char *filename, int flags) +{ + void *handle; + const int is_udev = filename && strcmp(filename, "libudev.so.1") == 0; + + if (resolve_real_functions() < 0) + return NULL; + + if (is_udev && udev_attempts++ < failure_attempt_limit()) { + const char *fake_udev = getenv("HIDAPI_TEST_FAKE_UDEV"); + + if (!fake_udev) + return NULL; + handle = real_dlopen(fake_udev, flags); + } + else { + handle = real_dlopen(filename, flags); + } + + if (is_udev && handle) { + udev_handle = handle; + ++udev_open_count; + } + + return handle; +} + +int dlclose(void *handle) +{ + if (resolve_real_functions() < 0) + return -1; + + if (handle == udev_handle) { + udev_handle = NULL; + ++udev_close_count; + } + + return real_dlclose(handle); +} + +unsigned int hidapi_test_udev_open_count(void) +{ + return udev_open_count; +} + +unsigned int hidapi_test_udev_close_count(void) +{ + return udev_close_count; +} diff --git a/linux/test/hidapi_hidraw_loader_test.c b/linux/test/hidapi_hidraw_loader_test.c new file mode 100644 index 000000000..265615958 --- /dev/null +++ b/linux/test/hidapi_hidraw_loader_test.c @@ -0,0 +1,164 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#define _GNU_SOURCE + +#include +#include +#include +#include + +#include "hidapi.h" + + +typedef unsigned int counter_(void); + +static int read_counter(const char *name, unsigned int *value) +{ + counter_ *counter; + void *symbol = dlsym(RTLD_DEFAULT, name); + + if (!symbol) { + fprintf(stderr, "unable to resolve test counter %s: %s\n", name, dlerror()); + return -1; + } + if (sizeof(counter) != sizeof(symbol)) { + fprintf(stderr, "test counter pointer size mismatch\n"); + return -1; + } + memcpy(&counter, &symbol, sizeof(symbol)); + *value = counter(); + return 0; +} + +static int verify_balanced_cleanup(unsigned int initial_opens, + unsigned int initial_closes, + unsigned int expected_cycles) +{ + unsigned int final_opens; + unsigned int final_closes; + + if (read_counter("hidapi_test_udev_open_count", &final_opens) < 0 || + read_counter("hidapi_test_udev_close_count", &final_closes) < 0) + return 1; + + if (final_opens - initial_opens != expected_cycles || + final_closes - initial_closes != expected_cycles) { + fprintf(stderr, + "unbalanced libudev lifecycle: expected %u cycles, observed %u opens and %u closes\n", + expected_cycles, + final_opens - initial_opens, + final_closes - initial_closes); + return 1; + } + + return 0; +} + +static int test_reload(void) +{ + int i; + unsigned int initial_opens; + unsigned int initial_closes; + + if (read_counter("hidapi_test_udev_open_count", &initial_opens) < 0 || + read_counter("hidapi_test_udev_close_count", &initial_closes) < 0) + return 1; + + for (i = 0; i < 3; ++i) { + if (hid_init() != 0) { + const wchar_t *error = hid_error(NULL); + fwprintf(stderr, L"hid_init failed: %ls\n", error ? error : L"(null)"); + return 1; + } + if (hid_exit() != 0) { + fprintf(stderr, "hid_exit failed\n"); + return 1; + } + } + + return verify_balanced_cleanup(initial_opens, initial_closes, 3); +} + +static int test_failure_recovery(void) +{ + int i; + unsigned int initial_opens; + unsigned int initial_closes; + + if (read_counter("hidapi_test_udev_open_count", &initial_opens) < 0 || + read_counter("hidapi_test_udev_close_count", &initial_closes) < 0) + return 1; + + for (i = 0; i < 2; ++i) { + const wchar_t *error; + + if (hid_init() != -1) { + fprintf(stderr, "hid_init unexpectedly succeeded with an incomplete libudev\n"); + return 1; + } + + error = hid_error(NULL); + if (!error || !wcsstr(error, L"udev_list_entry_get_name")) { + fwprintf(stderr, L"hid_init returned an unexpected error: %ls\n", error ? error : L"(null)"); + return 1; + } + } + + if (hid_init() != 0) { + const wchar_t *error = hid_error(NULL); + fwprintf(stderr, L"hid_init did not recover: %ls\n", error ? error : L"(null)"); + return 1; + } + if (hid_exit() != 0) { + fprintf(stderr, "hid_exit failed after recovery\n"); + return 1; + } + + if (hid_init() != 0) { + const wchar_t *error = hid_error(NULL); + fwprintf(stderr, L"hid_init failed after reload: %ls\n", error ? error : L"(null)"); + return 1; + } + if (hid_exit() != 0) { + fprintf(stderr, "final hid_exit failed\n"); + return 1; + } + + return verify_balanced_cleanup(initial_opens, initial_closes, 4); +} + +int main(int argc, char **argv) +{ + if (argc != 2) { + fprintf(stderr, "usage: %s reload|failure-recovery\n", argv[0]); + return 2; + } + + if (strcmp(argv[1], "reload") == 0) + return test_reload(); + if (strcmp(argv[1], "failure-recovery") == 0) + return test_failure_recovery(); + + fprintf(stderr, "unknown test mode: %s\n", argv[1]); + return 2; +} diff --git a/linux/test/incomplete_udev.c b/linux/test/incomplete_udev.c new file mode 100644 index 000000000..b033f549c --- /dev/null +++ b/linux/test/incomplete_udev.c @@ -0,0 +1,41 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#define HIDAPI_UDEV_TEST_STUB(name) void name(void) {} + +HIDAPI_UDEV_TEST_STUB(udev_new) +HIDAPI_UDEV_TEST_STUB(udev_unref) +HIDAPI_UDEV_TEST_STUB(udev_device_new_from_devnum) +HIDAPI_UDEV_TEST_STUB(udev_device_new_from_syspath) +HIDAPI_UDEV_TEST_STUB(udev_device_unref) +HIDAPI_UDEV_TEST_STUB(udev_device_get_parent_with_subsystem_devtype) +HIDAPI_UDEV_TEST_STUB(udev_device_get_syspath) +HIDAPI_UDEV_TEST_STUB(udev_device_get_devnode) +HIDAPI_UDEV_TEST_STUB(udev_device_get_sysattr_value) +HIDAPI_UDEV_TEST_STUB(udev_enumerate_new) +HIDAPI_UDEV_TEST_STUB(udev_enumerate_unref) +HIDAPI_UDEV_TEST_STUB(udev_enumerate_add_match_subsystem) +HIDAPI_UDEV_TEST_STUB(udev_enumerate_scan_devices) +HIDAPI_UDEV_TEST_STUB(udev_enumerate_get_list_entry) +HIDAPI_UDEV_TEST_STUB(udev_list_entry_get_next) + +/* udev_list_entry_get_name is intentionally missing. */ diff --git a/pc/hidapi-hidraw.pc.in b/pc/hidapi-hidraw.pc.in index 28b9fe681..bd2c6209d 100644 --- a/pc/hidapi-hidraw.pc.in +++ b/pc/hidapi-hidraw.pc.in @@ -8,4 +8,5 @@ Description: C Library for USB/Bluetooth HID device access from Linux, Mac OS X, URL: https://github.com/libusb/hidapi Version: @VERSION@ Libs: -L${libdir} -lhidapi-hidraw +Libs.private: -ldl Cflags: -I${includedir}/hidapi diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e663c3fd6..5e6182f1a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -104,7 +104,6 @@ set(EXPORT_COMPONENTS) set(HIDAPI_NEED_EXPORT_THREADS FALSE) set(HIDAPI_NEED_EXPORT_LIBUSB FALSE) -set(HIDAPI_NEED_EXPORT_LIBUDEV FALSE) set(HIDAPI_NEED_EXPORT_ICONV FALSE) if(WIN32) @@ -138,7 +137,6 @@ else() set(EXPORT_ALIAS hidraw) if(NOT BUILD_SHARED_LIBS) set(HIDAPI_NEED_EXPORT_THREADS TRUE) - set(HIDAPI_NEED_EXPORT_LIBUDEV TRUE) endif() endif() elseif(CMAKE_SYSTEM_NAME MATCHES "NetBSD") diff --git a/src/cmake/hidapi-config.cmake.in b/src/cmake/hidapi-config.cmake.in index 4226c8cd8..8888486bb 100644 --- a/src/cmake/hidapi-config.cmake.in +++ b/src/cmake/hidapi-config.cmake.in @@ -10,7 +10,6 @@ set(hidapi_FOUND FALSE) set(HIDAPI_NEED_EXPORT_THREADS @HIDAPI_NEED_EXPORT_THREADS@) set(HIDAPI_NEED_EXPORT_LIBUSB @HIDAPI_NEED_EXPORT_LIBUSB@) -set(HIDAPI_NEED_EXPORT_LIBUDEV @HIDAPI_NEED_EXPORT_LIBUDEV@) set(HIDAPI_NEED_EXPORT_ICONV @HIDAPI_NEED_EXPORT_ICONV@) if(HIDAPI_NEED_EXPORT_THREADS) @@ -20,17 +19,12 @@ if(HIDAPI_NEED_EXPORT_THREADS) find_package(Threads REQUIRED) endif() -if(HIDAPI_NEED_EXPORT_LIBUSB OR HIDAPI_NEED_EXPORT_LIBUDEV) +if(HIDAPI_NEED_EXPORT_LIBUSB) if(CMAKE_VERSION VERSION_LESS 3.6.3) message(FATAL_ERROR "This file relies on consumers using CMake 3.6.3 or greater.") endif() find_package(PkgConfig) - if(HIDAPI_NEED_EXPORT_LIBUSB) - pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0>=1.0.9) - endif() - if(HIDAPI_NEED_EXPORT_LIBUDEV) - pkg_check_modules(libudev REQUIRED IMPORTED_TARGET libudev) - endif() + pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0>=1.0.9) endif() if(HIDAPI_NEED_EXPORT_ICONV) From c2b0d410f406e9184f05d71231972fb2d3fc32af Mon Sep 17 00:00:00 2001 From: Ihor Dutchak Date: Fri, 31 Jul 2026 01:30:21 +0300 Subject: [PATCH 3/3] Update copyright year to 2026 Refresh copyright notices in the Linux loader sources changed by this PR. Assisted-by: codex:gpt-5.6-sol --- linux/hid.c | 2 +- linux/test/dlopen_interposer.c | 2 +- linux/test/hidapi_hidraw_loader_test.c | 2 +- linux/test/incomplete_udev.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/linux/hid.c b/linux/hid.c index 769f3726a..07b820df4 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -7,7 +7,7 @@ libusb/hidapi Team - Copyright 2022, All Rights Reserved. + Copyright 2026, All Rights Reserved. At the discretion of the user of this library, this software may be licensed under the terms of the diff --git a/linux/test/dlopen_interposer.c b/linux/test/dlopen_interposer.c index 16772a0f3..93623c573 100644 --- a/linux/test/dlopen_interposer.c +++ b/linux/test/dlopen_interposer.c @@ -4,7 +4,7 @@ libusb/hidapi Team - Copyright 2022, All Rights Reserved. + Copyright 2026, All Rights Reserved. At the discretion of the user of this library, this software may be licensed under the terms of the diff --git a/linux/test/hidapi_hidraw_loader_test.c b/linux/test/hidapi_hidraw_loader_test.c index c6a420d37..c9735217a 100644 --- a/linux/test/hidapi_hidraw_loader_test.c +++ b/linux/test/hidapi_hidraw_loader_test.c @@ -4,7 +4,7 @@ libusb/hidapi Team - Copyright 2022, All Rights Reserved. + Copyright 2026, All Rights Reserved. At the discretion of the user of this library, this software may be licensed under the terms of the diff --git a/linux/test/incomplete_udev.c b/linux/test/incomplete_udev.c index 2d55c1641..dc51927ff 100644 --- a/linux/test/incomplete_udev.c +++ b/linux/test/incomplete_udev.c @@ -4,7 +4,7 @@ libusb/hidapi Team - Copyright 2022, All Rights Reserved. + Copyright 2026, All Rights Reserved. At the discretion of the user of this library, this software may be licensed under the terms of the