Current section
Files
Jump to
Current section
Files
c_src/bodge_usb_nif.c
// SPDX-FileCopyrightText: 2026 Lars Wikman
//
// SPDX-License-Identifier: Apache-2.0
//
// bodge_usb syscall shim NIF.
//
// A narrow NIF over a single usbfs (or netlink) file descriptor: open/close/
// read/write, the fixed usbfs ioctls, and an async URB engine (submit / select
// / reap / discard). The fd lives in an ErlNifResource whose destructor closes
// it, so a dropped or GC'd handle never leaks an fd. errno is captured
// immediately after each syscall and mapped to an atom.
//
// Linux only. Fast, non-blocking calls (descriptor reads, URB submit/reap) run
// inline on a normal scheduler; blocking ioctls run on dirty I/O schedulers and
// must not hold the fd lock across the syscall -- they take a refcount and
// release the lock (begin_blocking/end_blocking). enif_select delivers URB
// readiness to the owning process.
#define _GNU_SOURCE // O_CLOEXEC and friends under -std=c11
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <unistd.h>
#include <linux/netlink.h>
#include <linux/usbdevice_fs.h>
#include <erl_nif.h>
// ---- resource type -------------------------------------------------------
// An in-flight async URB. `kurb` is the LAST member so an isochronous URB's
// trailing iso_frame_desc[] array can be over-allocated right after it. We
// recover the Urb from a reaped `struct usbdevfs_urb *` via kurb.usercontext
// (set to the owning Urb at submit). The kurb and its buffer must stay put until
// the URB is reaped or the fd is closed, so they live in enif_alloc memory
// tracked on the owning fd.
typedef struct Urb {
unsigned char *buffer;
size_t buffer_len;
size_t data_off; // offset of transfer data in buffer (8 for control's setup, else 0)
int is_in;
int num_packets; // 0 for bulk/interrupt; >0 for isochronous
ErlNifUInt64 tag; // caller correlation id
struct Urb *next;
struct Urb *prev;
struct usbdevfs_urb kurb;
} Urb;
typedef struct {
int fd; // -1 once closed
ErlNifMutex *lock; // serializes fd use vs close (no double-close/UAF)
Urb *inflight; // doubly-linked list of submitted-but-unreaped URBs
int select_active; // enif_select(WRITE) currently armed
int closing; // close requested; fd torn down once quiescent
int busy; // blocking ioctls in flight that released the lock
} UsbFd;
static ErlNifResourceType *usb_fd_type = NULL;
// Atoms, initialized in load().
static ERL_NIF_TERM am_ok;
static ERL_NIF_TERM am_error;
static ERL_NIF_TERM am_ebadf;
// ---- URB registry helpers (call with r->lock held) ---------------------
static void urb_link(UsbFd *r, Urb *u) {
u->prev = NULL;
u->next = r->inflight;
if (r->inflight)
r->inflight->prev = u;
r->inflight = u;
}
static void urb_unlink(UsbFd *r, Urb *u) {
if (u->prev)
u->prev->next = u->next;
else
r->inflight = u->next;
if (u->next)
u->next->prev = u->prev;
}
static void urb_free(Urb *u) {
if (u->buffer)
enif_free(u->buffer);
enif_free(u);
}
// Close the fd and drop all tracked URBs. Closing the usbfs fd cancels every
// kernel URB, so the kernel no longer references our Urb memory afterward and it
// is safe to free. Call with r->lock held.
static void teardown_fd(UsbFd *r) {
if (r->fd >= 0) {
close(r->fd);
r->fd = -1;
}
Urb *u = r->inflight;
while (u) {
Urb *n = u->next;
urb_free(u);
u = n;
}
r->inflight = NULL;
}
// Finalize a deferred close: actually tear the fd down once nothing is using it
// (no select armed, no blocking ioctl in flight). Call with r->lock held.
static void try_close_locked(UsbFd *r) {
if (r->closing && !r->select_active && r->busy == 0)
teardown_fd(r);
}
// Take a reference for a blocking ioctl and hand back the fd to use WITHOUT
// holding the lock across the syscall -- otherwise a normal-scheduler NIF
// (reap/submit/close) on the same handle would stall behind it, violating the
// scheduler contract. Returns -1 if the fd is closed or a close is pending.
static int begin_blocking(UsbFd *r) {
enif_mutex_lock(r->lock);
if (r->fd < 0 || r->closing) {
enif_mutex_unlock(r->lock);
return -1;
}
r->busy++;
int fd = r->fd;
enif_mutex_unlock(r->lock);
return fd;
}
// Release a begin_blocking() reference; finalize a close that waited on us.
static void end_blocking(UsbFd *r) {
enif_mutex_lock(r->lock);
r->busy--;
try_close_locked(r);
enif_mutex_unlock(r->lock);
}
static ERL_NIF_TERM mk_atom(ErlNifEnv *env, const char *s) {
ERL_NIF_TERM a;
if (enif_make_existing_atom(env, s, &a, ERL_NIF_LATIN1))
return a;
return enif_make_atom(env, s);
}
// Map errno -> atom. Common cases get a name; anything else becomes :eNNN so the
// caller always receives an atom and never loses information.
static ERL_NIF_TERM errno_atom(ErlNifEnv *env, int e) {
const char *name = NULL;
switch (e) {
case EPERM: name = "eperm"; break;
case ENOENT: name = "enoent"; break;
case EINTR: name = "eintr"; break;
case EIO: name = "eio"; break;
case ENXIO: name = "enxio"; break;
case EBADF: name = "ebadf"; break;
case EAGAIN: name = "eagain"; break;
case ENOMEM: name = "enomem"; break;
case EACCES: name = "eacces"; break;
case EFAULT: name = "efault"; break;
case EBUSY: name = "ebusy"; break;
case ENODEV: name = "enodev"; break;
case EINVAL: name = "einval"; break;
case ENOTTY: name = "enotty"; break;
case EPIPE: name = "epipe"; break;
case ENOSPC: name = "enospc"; break;
case EOVERFLOW: name = "eoverflow"; break;
case ETIMEDOUT: name = "etimedout"; break;
case ECONNRESET: name = "econnreset"; break;
case ESHUTDOWN: name = "eshutdown"; break;
case EPROTO: name = "eproto"; break;
case EILSEQ: name = "eilseq"; break;
case EXDEV: name = "exdev"; break; // USB isoc: partial completion
case ENODATA: name = "enodata"; break; // GETDRIVER: no driver bound
case ETIME: name = "etime"; break; // USB isoc/interrupt timeout
case EREMOTEIO: name = "eremoteio"; break; // USB short read
case ENOSYS: name = "enosys"; break;
case EMFILE: name = "emfile"; break;
case ENFILE: name = "enfile"; break;
default: break;
}
if (name)
return mk_atom(env, name);
char buf[16];
snprintf(buf, sizeof(buf), "e%d", e);
return enif_make_atom(env, buf);
}
static ERL_NIF_TERM err_tuple(ErlNifEnv *env, int e) {
return enif_make_tuple2(env, am_error, errno_atom(env, e));
}
// enif_select stop callback: runs when it is safe to close the fd (no in-flight
// select). This is where an fd that was ever selected upon is actually closed.
static void usb_fd_stop(ErlNifEnv *env, void *obj, ErlNifEvent event, int is_direct_call) {
(void)env;
(void)event;
(void)is_direct_call;
UsbFd *r = (UsbFd *)obj;
enif_mutex_lock(r->lock);
r->select_active = 0;
// A blocking ioctl may still be mid-syscall (it released the lock); if so,
// its end_blocking() does the teardown. Otherwise finalize here.
if (r->busy == 0)
teardown_fd(r);
enif_mutex_unlock(r->lock);
}
static void usb_fd_dtor(ErlNifEnv *env, void *obj) {
(void)env;
UsbFd *r = (UsbFd *)obj;
// If the fd was ever selected upon, the stop callback already tore it down
// (enif_select keeps the resource alive until STOP completes, so stop runs
// before dtor). Otherwise close it here. No other references remain.
if (r->fd >= 0)
teardown_fd(r);
if (r->lock) {
enif_mutex_destroy(r->lock);
r->lock = NULL;
}
}
// ---- open flags ----------------------------------------------------------
// Translate a list of flag atoms to open(2) flags. O_CLOEXEC is always set so
// descriptors never leak across an exec. Returns -1 on a bad flag.
static int parse_open_flags(ErlNifEnv *env, ERL_NIF_TERM list, int *out) {
int flags = O_CLOEXEC;
int have_access = 0;
ERL_NIF_TERM head, tail = list;
char name[16];
while (enif_get_list_cell(env, tail, &head, &tail)) {
if (enif_get_atom(env, head, name, sizeof(name), ERL_NIF_LATIN1) <= 0)
return -1;
if (strcmp(name, "rdonly") == 0) { flags |= O_RDONLY; have_access = 1; }
else if (strcmp(name, "wronly") == 0) { flags |= O_WRONLY; have_access = 1; }
else if (strcmp(name, "rdwr") == 0) { flags |= O_RDWR; have_access = 1; }
else if (strcmp(name, "nonblock") == 0) flags |= O_NONBLOCK;
else return -1;
}
if (!have_access)
flags |= O_RDWR; // sensible default for usbfs
*out = flags;
return 0;
}
// Bounded unsigned getters: reject out-of-range values as badarg so a caller
// can never overflow a usbfs struct field.
static int get_bounded(ErlNifEnv *env, ERL_NIF_TERM t, unsigned max, unsigned *out) {
unsigned v;
if (!enif_get_uint(env, t, &v) || v > max)
return 0;
*out = v;
return 1;
}
// Wrap an already-open fd (usbfs node or netlink socket) in a UsbFd resource and
// return {:ok, handle}. Takes ownership of fd: it is closed on error.
static ERL_NIF_TERM wrap_fd(ErlNifEnv *env, int fd) {
UsbFd *r = enif_alloc_resource(usb_fd_type, sizeof(UsbFd));
if (!r) {
close(fd);
return err_tuple(env, ENOMEM);
}
r->fd = fd;
r->inflight = NULL;
r->select_active = 0;
r->closing = 0;
r->busy = 0;
r->lock = enif_mutex_create("bodge_usb_fd");
if (!r->lock) {
close(fd);
r->fd = -1;
enif_release_resource(r);
return err_tuple(env, ENOMEM);
}
ERL_NIF_TERM term = enif_make_resource(env, r);
enif_release_resource(r); // the term now owns the only reference
return enif_make_tuple2(env, am_ok, term);
}
// ---- NIFs ----------------------------------------------------------------
// open(path :: binary, flags :: [atom]) -> {:ok, handle} | {:error, atom}
static ERL_NIF_TERM nif_open(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
ErlNifBinary path;
if (!enif_inspect_binary(env, argv[0], &path) &&
!enif_inspect_iolist_as_binary(env, argv[0], &path))
return enif_make_badarg(env);
if (path.size == 0 || path.size > 4095)
return enif_make_badarg(env);
int flags;
if (parse_open_flags(env, argv[1], &flags) != 0)
return enif_make_badarg(env);
// NUL-terminate the path.
char cpath[4096];
memcpy(cpath, path.data, path.size);
cpath[path.size] = '\0';
int fd = open(cpath, flags);
if (fd < 0)
return err_tuple(env, errno);
return wrap_fd(env, fd);
}
// close(handle) -> :ok | {:error, atom} (idempotent)
//
// If the fd was ever armed for select, tear it down via ERL_NIF_SELECT_STOP so
// the fd is removed from the poller before close() -- an in-flight select over a
// closed fd is a bug class. The actual close + URB cleanup happens in the stop
// callback. Otherwise close inline.
static ERL_NIF_TERM nif_close(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r))
return enif_make_badarg(env);
enif_mutex_lock(r->lock);
if (r->closing || r->fd < 0) {
enif_mutex_unlock(r->lock);
return am_ok;
}
r->closing = 1;
int select_active = r->select_active;
int fd = r->fd;
if (select_active) {
// Release the lock BEFORE enif_select(STOP): the stop callback may run
// synchronously in this thread and it takes r->lock -- holding it here
// would deadlock. `closing` already blocks any concurrent re-entry, and
// usb_fd_stop() does the actual close + URB teardown under the lock.
enif_mutex_unlock(r->lock);
enif_select(env, (ErlNifEvent)fd, ERL_NIF_SELECT_STOP, r, NULL,
enif_make_atom(env, "undefined"));
return am_ok;
}
// Not selected on: close now if quiescent. If a blocking ioctl is mid-flight
// (it released the lock and is using the fd), defer the teardown to its
// end_blocking() so we never close the fd out from under an active syscall.
try_close_locked(r);
enif_mutex_unlock(r->lock);
return am_ok;
}
// read(handle, count :: non_neg_integer) -> {:ok, binary} | {:error, atom}
static ERL_NIF_TERM nif_read(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
unsigned long count;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r))
return enif_make_badarg(env);
if (!enif_get_ulong(env, argv[1], &count))
return enif_make_badarg(env);
if (count > (16u * 1024 * 1024))
return enif_make_badarg(env); // sanity cap; descriptors are tiny
ErlNifBinary bin;
if (!enif_alloc_binary((size_t)count, &bin))
return err_tuple(env, ENOMEM);
ssize_t n;
int e = 0;
enif_mutex_lock(r->lock);
if (r->fd < 0 || r->closing) {
enif_mutex_unlock(r->lock);
enif_release_binary(&bin);
return enif_make_tuple2(env, am_error, am_ebadf);
}
n = read(r->fd, bin.data, (size_t)count);
e = errno;
enif_mutex_unlock(r->lock);
if (n < 0) {
enif_release_binary(&bin);
return err_tuple(env, e);
}
if ((size_t)n != bin.size) {
if (!enif_realloc_binary(&bin, (size_t)n)) {
enif_release_binary(&bin);
return err_tuple(env, ENOMEM);
}
}
return enif_make_tuple2(env, am_ok, enif_make_binary(env, &bin));
}
// write(handle, data :: iodata) -> {:ok, bytes_written} | {:error, atom}
static ERL_NIF_TERM nif_write(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
ErlNifBinary data;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r))
return enif_make_badarg(env);
if (!enif_inspect_iolist_as_binary(env, argv[1], &data))
return enif_make_badarg(env);
ssize_t n;
int e = 0;
enif_mutex_lock(r->lock);
if (r->fd < 0 || r->closing) {
enif_mutex_unlock(r->lock);
return enif_make_tuple2(env, am_error, am_ebadf);
}
n = write(r->fd, data.data, data.size);
e = errno;
enif_mutex_unlock(r->lock);
if (n < 0)
return err_tuple(env, e);
return enif_make_tuple2(env, am_ok, enif_make_ulong(env, (unsigned long)n));
}
// USBDEVFS_CONTROL wLength is a __u16, so transfers are bounded at 65535 bytes.
#define CTRL_MAX_LEN 0xFFFF
// set_interface(handle, interface, altsetting) -> :ok | {:error, atom}
// USBDEVFS_SETINTERFACE; no data buffer, but it drives a SET_INTERFACE request
// to the device, so it can block -> dirty I/O scheduler.
static ERL_NIF_TERM nif_set_interface(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
unsigned iface, alt;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r) ||
!enif_get_uint(env, argv[1], &iface) ||
!enif_get_uint(env, argv[2], &alt))
return enif_make_badarg(env);
struct usbdevfs_setinterface si;
memset(&si, 0, sizeof(si));
si.interface = iface;
si.altsetting = alt;
int rc, e = 0;
int fd = begin_blocking(r);
if (fd < 0)
return enif_make_tuple2(env, am_error, am_ebadf);
rc = ioctl(fd, USBDEVFS_SETINTERFACE, &si);
e = errno;
end_blocking(r);
if (rc < 0)
return err_tuple(env, e);
return am_ok;
}
// Bulk transfers can be large; cap allocations at a sane ceiling.
#define BULK_MAX_LEN (16u * 1024 * 1024)
// A fast fd bookkeeping ioctl whose only argument is an unsigned int passed by
// reference (claim/release interface, clear halt). Runs inline.
static ERL_NIF_TERM uint_ioctl(ErlNifEnv *env, const ERL_NIF_TERM argv[],
unsigned long request, int blocking) {
UsbFd *r;
unsigned value;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r) ||
!enif_get_uint(env, argv[1], &value))
return enif_make_badarg(env);
unsigned int arg = value;
int rc, e = 0;
if (blocking) {
// Drives a device round-trip (CLEAR_HALT); release the lock so a
// normal-scheduler NIF on this fd never stalls behind the syscall.
int fd = begin_blocking(r);
if (fd < 0)
return enif_make_tuple2(env, am_error, am_ebadf);
rc = ioctl(fd, request, &arg);
e = errno;
end_blocking(r);
} else {
// Fast fd bookkeeping (claim/release): a brief lock is fine.
enif_mutex_lock(r->lock);
if (r->fd < 0 || r->closing) {
enif_mutex_unlock(r->lock);
return enif_make_tuple2(env, am_error, am_ebadf);
}
rc = ioctl(r->fd, request, &arg);
e = errno;
enif_mutex_unlock(r->lock);
}
if (rc < 0)
return err_tuple(env, e);
return am_ok;
}
// claim_interface(handle, interface) -> :ok | {:error, atom}
static ERL_NIF_TERM nif_claim_interface(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
return uint_ioctl(env, argv, USBDEVFS_CLAIMINTERFACE, 0);
}
// release_interface(handle, interface) -> :ok | {:error, atom}
static ERL_NIF_TERM nif_release_interface(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
return uint_ioctl(env, argv, USBDEVFS_RELEASEINTERFACE, 0);
}
// ---- kernel driver detach/reattach ------------------------------------
// get_driver(handle, interface) -> {:ok, name} | {:error, atom}
// Name of the kernel driver bound to the interface, or :enodata if none.
static ERL_NIF_TERM nif_get_driver(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
unsigned iface;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r) ||
!enif_get_uint(env, argv[1], &iface))
return enif_make_badarg(env);
struct usbdevfs_getdriver gd;
memset(&gd, 0, sizeof(gd));
gd.interface = iface;
int rc, e = 0;
enif_mutex_lock(r->lock);
if (r->fd < 0 || r->closing) {
enif_mutex_unlock(r->lock);
return enif_make_tuple2(env, am_error, am_ebadf);
}
rc = ioctl(r->fd, USBDEVFS_GETDRIVER, &gd);
e = errno;
enif_mutex_unlock(r->lock);
if (rc < 0)
return err_tuple(env, e);
gd.driver[USBDEVFS_MAXDRIVERNAME] = '\0';
ERL_NIF_TERM name;
size_t len = strlen(gd.driver);
unsigned char *buf = enif_make_new_binary(env, len, &name);
memcpy(buf, gd.driver, len);
return enif_make_tuple2(env, am_ok, name);
}
// Wrap a nested usbfs ioctl (DISCONNECT/CONNECT) targeting an interface, sent
// via USBDEVFS_IOCTL. Detach/attach drive the kernel driver's disconnect/probe,
// so they can block -> dirty I/O.
static ERL_NIF_TERM driver_ioctl(ErlNifEnv *env, const ERL_NIF_TERM argv[], int nested_code) {
UsbFd *r;
unsigned iface;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r) ||
!enif_get_uint(env, argv[1], &iface))
return enif_make_badarg(env);
struct usbdevfs_ioctl cmd;
memset(&cmd, 0, sizeof(cmd));
cmd.ifno = (int)iface;
cmd.ioctl_code = nested_code;
cmd.data = NULL;
// Detach/attach drive the kernel driver's disconnect/probe: blocking, so
// release the lock across the syscall like the other dirty ioctls.
int rc, e = 0;
int fd = begin_blocking(r);
if (fd < 0)
return enif_make_tuple2(env, am_error, am_ebadf);
rc = ioctl(fd, USBDEVFS_IOCTL, &cmd);
e = errno;
end_blocking(r);
if (rc < 0)
return err_tuple(env, e);
return am_ok;
}
// detach_driver(handle, interface) -> :ok | {:error, atom}
static ERL_NIF_TERM nif_detach_driver(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
return driver_ioctl(env, argv, USBDEVFS_DISCONNECT);
}
// clear_halt(handle, endpoint) -> :ok | {:error, atom}
// Clear an endpoint's halt/stall (USBDEVFS_CLEAR_HALT sends CLEAR_FEATURE
// ENDPOINT_HALT). Drives a control request, so dirty I/O.
static ERL_NIF_TERM nif_clear_halt(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
return uint_ioctl(env, argv, USBDEVFS_CLEAR_HALT, 1);
}
// reset(handle) -> :ok | {:error, atom}
// Reset the device (USBDEVFS_RESET). Re-enumerates, so dirty I/O; the device may
// come back with a new address, invalidating this handle.
static ERL_NIF_TERM nif_reset(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r))
return enif_make_badarg(env);
int rc, e = 0;
int fd = begin_blocking(r);
if (fd < 0)
return enif_make_tuple2(env, am_error, am_ebadf);
rc = ioctl(fd, USBDEVFS_RESET);
e = errno;
end_blocking(r);
if (rc < 0)
return err_tuple(env, e);
return am_ok;
}
// attach_driver(handle, interface) -> :ok | {:error, atom}
static ERL_NIF_TERM nif_attach_driver(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
return driver_ioctl(env, argv, USBDEVFS_CONNECT);
}
// ---- async engine: submit / select / reap / discard -------------------
// A completed URB's status: 0 -> :ok, negative -> errno atom of its magnitude.
static ERL_NIF_TERM urb_status_term(ErlNifEnv *env, int status) {
if (status == 0)
return am_ok;
return errno_atom(env, status < 0 ? -status : status);
}
// submit_urb(handle, tag :: u64, urb_type, endpoint, data_or_length, flags)
// -> :ok | {:error, atom}
// Fast, non-blocking SUBMITURB: hands a URB (bulk or interrupt) to the kernel
// and returns immediately. urb_type is USBDEVFS_URB_TYPE_BULK/INTERRUPT. flags is
// a subset of {USBDEVFS_URB_ZERO_PACKET} -- a terminating zero-length packet for
// OUT transfers. The URB (and its buffer) are tracked on the fd until reaped.
static ERL_NIF_TERM nif_submit(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
ErlNifUInt64 tag;
unsigned urb_type, ep, flags;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r) ||
!enif_get_uint64(env, argv[1], &tag) ||
!get_bounded(env, argv[2], 0xFF, &urb_type) ||
!get_bounded(env, argv[3], 0xFF, &ep) ||
!enif_get_uint(env, argv[5], &flags))
return enif_make_badarg(env);
if (urb_type != USBDEVFS_URB_TYPE_BULK && urb_type != USBDEVFS_URB_TYPE_INTERRUPT)
return enif_make_badarg(env);
if (flags & ~(unsigned)USBDEVFS_URB_ZERO_PACKET)
return enif_make_badarg(env);
int is_in = (ep & 0x80) != 0;
ErlNifBinary out_data = {0};
size_t len;
if (is_in) {
unsigned long n;
if (!enif_get_ulong(env, argv[4], &n) || n > BULK_MAX_LEN)
return enif_make_badarg(env);
len = (size_t)n;
} else {
if (!enif_inspect_iolist_as_binary(env, argv[4], &out_data) ||
out_data.size > BULK_MAX_LEN)
return enif_make_badarg(env);
len = out_data.size;
}
Urb *u = enif_alloc(sizeof(Urb));
if (!u)
return err_tuple(env, ENOMEM);
memset(u, 0, sizeof(*u));
u->buffer_len = len;
u->is_in = is_in;
u->tag = tag;
if (len) {
u->buffer = enif_alloc(len);
if (!u->buffer) {
enif_free(u);
return err_tuple(env, ENOMEM);
}
if (!is_in)
memcpy(u->buffer, out_data.data, len);
}
u->kurb.type = (unsigned char)urb_type;
u->kurb.endpoint = (unsigned char)ep;
u->kurb.flags = flags;
u->kurb.buffer = u->buffer;
u->kurb.buffer_length = (int)len;
u->kurb.usercontext = u;
enif_mutex_lock(r->lock);
if (r->fd < 0 || r->closing) {
enif_mutex_unlock(r->lock);
urb_free(u);
return enif_make_tuple2(env, am_error, am_ebadf);
}
int rc = ioctl(r->fd, USBDEVFS_SUBMITURB, &u->kurb);
int e = errno;
if (rc < 0) {
enif_mutex_unlock(r->lock);
urb_free(u);
return err_tuple(env, e);
}
urb_link(r, u);
enif_mutex_unlock(r->lock);
return am_ok;
}
// Submit a control transfer as an async URB so the engine never blocks on it.
// The buffer is [8-byte setup packet][wLength data]; the kernel reports the data
// length (excluding setup) in actual_length, and reap reads IN data at +8.
// args: (handle, tag, bmRequestType, bRequest, wValue, wIndex, data_or_length)
static ERL_NIF_TERM nif_submit_control(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
ErlNifUInt64 tag;
unsigned rtype, req, wvalue, windex;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r) ||
!enif_get_uint64(env, argv[1], &tag) ||
!get_bounded(env, argv[2], 0xFF, &rtype) ||
!get_bounded(env, argv[3], 0xFF, &req) ||
!get_bounded(env, argv[4], 0xFFFF, &wvalue) ||
!get_bounded(env, argv[5], 0xFFFF, &windex))
return enif_make_badarg(env);
int is_in = (rtype & 0x80) != 0;
ErlNifBinary out_data = {0};
size_t wlen;
if (is_in) {
unsigned long n;
if (!enif_get_ulong(env, argv[6], &n) || n > CTRL_MAX_LEN)
return enif_make_badarg(env);
wlen = (size_t)n;
} else {
if (!enif_inspect_iolist_as_binary(env, argv[6], &out_data) ||
out_data.size > CTRL_MAX_LEN)
return enif_make_badarg(env);
wlen = out_data.size;
}
size_t total = 8 + wlen;
Urb *u = enif_alloc(sizeof(Urb));
if (!u)
return err_tuple(env, ENOMEM);
memset(u, 0, sizeof(*u));
u->buffer_len = total;
u->data_off = 8;
u->is_in = is_in;
u->tag = tag;
u->buffer = enif_alloc(total);
if (!u->buffer) {
enif_free(u);
return err_tuple(env, ENOMEM);
}
// Setup packet (little-endian wValue/wIndex/wLength).
u->buffer[0] = (unsigned char)rtype;
u->buffer[1] = (unsigned char)req;
u->buffer[2] = (unsigned char)(wvalue & 0xff);
u->buffer[3] = (unsigned char)(wvalue >> 8);
u->buffer[4] = (unsigned char)(windex & 0xff);
u->buffer[5] = (unsigned char)(windex >> 8);
u->buffer[6] = (unsigned char)(wlen & 0xff);
u->buffer[7] = (unsigned char)(wlen >> 8);
if (wlen) {
if (is_in)
memset(u->buffer + 8, 0, wlen); // never leak heap into the IN binary
else
memcpy(u->buffer + 8, out_data.data, wlen);
}
u->kurb.type = USBDEVFS_URB_TYPE_CONTROL;
u->kurb.endpoint = 0; // direction lives in the setup packet's bmRequestType
u->kurb.flags = 0;
u->kurb.buffer = u->buffer;
u->kurb.buffer_length = (int)total;
u->kurb.usercontext = u;
enif_mutex_lock(r->lock);
if (r->fd < 0 || r->closing) {
enif_mutex_unlock(r->lock);
urb_free(u);
return enif_make_tuple2(env, am_error, am_ebadf);
}
int rc = ioctl(r->fd, USBDEVFS_SUBMITURB, &u->kurb);
int e = errno;
if (rc < 0) {
enif_mutex_unlock(r->lock);
urb_free(u);
return err_tuple(env, e);
}
urb_link(r, u);
enif_mutex_unlock(r->lock);
return am_ok;
}
// Max isochronous packets per URB (usbfs caps at 128; keep a bit of headroom).
#define ISO_MAX_PACKETS 128
// submit_iso(handle, tag :: u64, endpoint, packet_lengths :: [uint], out_data)
// -> :ok | {:error, atom}
// Isochronous URB. packet_lengths gives the per-packet byte counts (and thus the
// packet count and total buffer size). For IN, out_data is ignored and each
// packet reads up to its length; for OUT, out_data must be exactly the total.
// Scheduled ASAP. Reaped by reap/1 with per-packet {actual_length, status}.
static ERL_NIF_TERM nif_submit_iso(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
ErlNifUInt64 tag;
unsigned ep, count;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r) ||
!enif_get_uint64(env, argv[1], &tag) ||
!get_bounded(env, argv[2], 0xFF, &ep) ||
!enif_get_list_length(env, argv[3], &count) ||
count == 0 || count > ISO_MAX_PACKETS)
return enif_make_badarg(env);
// Read the per-packet lengths and total.
unsigned lengths[ISO_MAX_PACKETS];
size_t total = 0;
ERL_NIF_TERM head, tail = argv[3];
for (unsigned i = 0; i < count; i++) {
unsigned l;
if (!enif_get_list_cell(env, tail, &head, &tail) || !enif_get_uint(env, head, &l) ||
l > 0xFFFF)
return enif_make_badarg(env);
lengths[i] = l;
total += l;
}
if (total > BULK_MAX_LEN)
return enif_make_badarg(env);
int is_in = (ep & 0x80) != 0;
ErlNifBinary out_data = {0};
if (!is_in) {
if (!enif_inspect_iolist_as_binary(env, argv[4], &out_data) || out_data.size != total)
return enif_make_badarg(env);
}
// Over-allocate the Urb so kurb's trailing iso_frame_desc[count] fits.
size_t urb_size = sizeof(Urb) + (size_t)count * sizeof(struct usbdevfs_iso_packet_desc);
Urb *u = enif_alloc(urb_size);
if (!u)
return err_tuple(env, ENOMEM);
memset(u, 0, urb_size);
u->buffer_len = total;
u->is_in = is_in;
u->num_packets = (int)count;
u->tag = tag;
if (total) {
u->buffer = enif_alloc(total);
if (!u->buffer) {
enif_free(u);
return err_tuple(env, ENOMEM);
}
if (is_in)
// Reap returns the whole buffer for isoc IN, but the kernel only
// writes each packet's actual_length bytes -- zero the gaps so
// uninitialized heap never leaks into the returned binary.
memset(u->buffer, 0, total);
else
memcpy(u->buffer, out_data.data, total);
}
u->kurb.type = USBDEVFS_URB_TYPE_ISO;
u->kurb.endpoint = (unsigned char)ep;
u->kurb.flags = USBDEVFS_URB_ISO_ASAP;
u->kurb.buffer = u->buffer;
u->kurb.buffer_length = (int)total;
u->kurb.number_of_packets = (int)count;
u->kurb.usercontext = u;
for (unsigned i = 0; i < count; i++)
u->kurb.iso_frame_desc[i].length = lengths[i];
enif_mutex_lock(r->lock);
if (r->fd < 0 || r->closing) {
enif_mutex_unlock(r->lock);
urb_free(u);
return enif_make_tuple2(env, am_error, am_ebadf);
}
int rc = ioctl(r->fd, USBDEVFS_SUBMITURB, &u->kurb);
int e = errno;
if (rc < 0) {
enif_mutex_unlock(r->lock);
urb_free(u);
return err_tuple(env, e);
}
urb_link(r, u);
enif_mutex_unlock(r->lock);
return am_ok;
}
// Arm enif_select on the fd; the two NIFs below differ only in mode.
static ERL_NIF_TERM do_select(ErlNifEnv *env, const ERL_NIF_TERM argv[],
enum ErlNifSelectFlags mode) {
UsbFd *r;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r))
return enif_make_badarg(env);
enif_mutex_lock(r->lock);
if (r->fd < 0 || r->closing) {
enif_mutex_unlock(r->lock);
return enif_make_tuple2(env, am_error, am_ebadf);
}
int rc = enif_select(env, (ErlNifEvent)r->fd, mode, r, NULL, argv[1]);
if (rc >= 0)
r->select_active = 1;
enif_mutex_unlock(r->lock);
if (rc < 0)
return enif_make_tuple2(env, am_error, enif_make_atom(env, "eselect"));
return am_ok;
}
// select(handle, ref) -> :ok | {:error, atom}
// Arm enif_select for write-readiness: usbfs reports POLLOUT when a URB is
// reapable. The calling process gets `{:select, handle, ref, :ready_output}`.
static ERL_NIF_TERM nif_select(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
return do_select(env, argv, ERL_NIF_SELECT_WRITE);
}
// select_read(handle, ref) -> :ok | {:error, atom}
// Arm enif_select for read-readiness (POLLIN). Used for the hotplug netlink
// socket. The caller gets `{:select, handle, ref, :ready_input}`.
static ERL_NIF_TERM nif_select_read(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
return do_select(env, argv, ERL_NIF_SELECT_READ);
}
// netlink_uevent_open() -> {:ok, handle} | {:error, atom}
// Open a NETLINK_KOBJECT_UEVENT socket bound to the kernel uevent broadcast
// group. read/1 returns one uevent datagram; select_read/2 signals readiness.
// Non-blocking + cloexec. Usually needs root / CAP_NET_ADMIN.
static ERL_NIF_TERM nif_netlink_uevent_open(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
(void)argc;
(void)argv;
int fd = socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_KOBJECT_UEVENT);
if (fd < 0)
return err_tuple(env, errno);
struct sockaddr_nl addr;
memset(&addr, 0, sizeof(addr));
addr.nl_family = AF_NETLINK;
addr.nl_pid = 0; // let the kernel assign a unique pid
addr.nl_groups = 1; // group 1 == the uevent multicast group
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
int e = errno;
close(fd);
return err_tuple(env, e);
}
return wrap_fd(env, fd);
}
// Read one uevent datagram, verifying the kernel sent it. A netlink message
// from the kernel has source nl_pid == 0; a user process (with CAP_NET_ADMIN,
// or racing the same multicast group) would have nl_pid != 0 and could spoof
// hotplug events. libudev makes the same check via recvmsg. A non-kernel or
// address-less datagram is dropped -- returned as an empty binary so the caller
// keeps draining rather than trusting it. Otherwise like nif_read.
static ERL_NIF_TERM nif_netlink_read(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
unsigned long count;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r) ||
!enif_get_ulong(env, argv[1], &count))
return enif_make_badarg(env);
if (count > (16u * 1024 * 1024))
return enif_make_badarg(env);
ErlNifBinary bin;
if (!enif_alloc_binary((size_t)count, &bin))
return err_tuple(env, ENOMEM);
struct sockaddr_nl src;
memset(&src, 0, sizeof(src));
struct iovec iov = {.iov_base = bin.data, .iov_len = (size_t)count};
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = &src;
msg.msg_namelen = sizeof(src);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ssize_t n;
int e = 0;
enif_mutex_lock(r->lock);
if (r->fd < 0 || r->closing) {
enif_mutex_unlock(r->lock);
enif_release_binary(&bin);
return enif_make_tuple2(env, am_error, am_ebadf);
}
n = recvmsg(r->fd, &msg, 0);
e = errno;
enif_mutex_unlock(r->lock);
if (n < 0) {
enif_release_binary(&bin);
return err_tuple(env, e);
}
// Trust only a full sockaddr_nl whose nl_pid is 0 (the kernel). Drop
// anything else as an empty payload.
if (msg.msg_namelen != sizeof(src) || src.nl_pid != 0) {
enif_release_binary(&bin);
ERL_NIF_TERM empty;
(void)enif_make_new_binary(env, 0, &empty);
return enif_make_tuple2(env, am_ok, empty);
}
if ((size_t)n != bin.size) {
if (!enif_realloc_binary(&bin, (size_t)n)) {
enif_release_binary(&bin);
return err_tuple(env, ENOMEM);
}
}
return enif_make_tuple2(env, am_ok, enif_make_binary(env, &bin));
}
// reap(handle) -> [{tag :: u64, status :: :ok | atom, data_or_actual_length}]
// Drains all currently-completed URBs with the non-blocking REAPURBNDELAY. For
// IN URBs the third element is the received binary; for OUT it is the actual
// length written. Runs inline (each reap is non-blocking).
static ERL_NIF_TERM nif_reap(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r))
return enif_make_badarg(env);
ERL_NIF_TERM list = enif_make_list(env, 0);
enif_mutex_lock(r->lock);
if (r->fd < 0 || r->closing) {
enif_mutex_unlock(r->lock);
return list;
}
for (;;) {
struct usbdevfs_urb *ku = NULL;
int rc = ioctl(r->fd, USBDEVFS_REAPURBNDELAY, &ku);
if (rc < 0)
break; // EAGAIN (none ready) or a terminal error; stop draining
Urb *u = (Urb *)ku->usercontext;
ERL_NIF_TERM payload;
if (u->num_packets > 0) {
// Isochronous: {:iso, data_or_actual, [{actual_length, status}, ...]}.
ERL_NIF_TERM plist = enif_make_list(env, 0);
for (int i = u->num_packets - 1; i >= 0; i--) {
struct usbdevfs_iso_packet_desc *pd = &u->kurb.iso_frame_desc[i];
ERL_NIF_TERM pe = enif_make_tuple2(env, enif_make_uint(env, pd->actual_length),
urb_status_term(env, (int)pd->status));
plist = enif_make_list_cell(env, pe, plist);
}
ERL_NIF_TERM data;
if (u->is_in) {
ErlNifBinary b;
if (!enif_alloc_binary(u->buffer_len, &b)) {
// Keep the IN contract (a binary), just empty on OOM.
(void)enif_make_new_binary(env, 0, &data);
} else {
if (u->buffer_len)
memcpy(b.data, u->buffer, u->buffer_len);
data = enif_make_binary(env, &b);
}
} else {
data = enif_make_int(env, u->kurb.actual_length);
}
payload = enif_make_tuple3(env, mk_atom(env, "iso"), data, plist);
} else if (u->is_in) {
size_t got = u->kurb.actual_length > 0 ? (size_t)u->kurb.actual_length : 0;
ErlNifBinary b;
if (!enif_alloc_binary(got, &b)) {
// Keep the IN contract (a binary), just empty on OOM.
(void)enif_make_new_binary(env, 0, &payload);
} else {
// For control URBs the data sits after the 8-byte setup packet.
if (got)
memcpy(b.data, u->buffer + u->data_off, got);
payload = enif_make_binary(env, &b);
}
} else {
payload = enif_make_int(env, u->kurb.actual_length);
}
ERL_NIF_TERM entry = enif_make_tuple3(
env, enif_make_uint64(env, u->tag), urb_status_term(env, u->kurb.status), payload);
list = enif_make_list_cell(env, entry, list);
urb_unlink(r, u);
urb_free(u);
}
enif_mutex_unlock(r->lock);
return list;
}
// discard(handle, tag) -> :ok | {:error, :enoent} | {:error, atom}
// Cancel an in-flight URB. It still completes (with an ECONNRESET status) and is
// delivered by the next reap, which is where its memory is reclaimed.
static ERL_NIF_TERM nif_discard(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
ErlNifUInt64 tag;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r) ||
!enif_get_uint64(env, argv[1], &tag))
return enif_make_badarg(env);
enif_mutex_lock(r->lock);
if (r->fd < 0 || r->closing) {
enif_mutex_unlock(r->lock);
return enif_make_tuple2(env, am_error, am_ebadf);
}
Urb *u = r->inflight;
while (u && u->tag != tag)
u = u->next;
if (!u) {
enif_mutex_unlock(r->lock);
return enif_make_tuple2(env, am_error, enif_make_atom(env, "enoent"));
}
int rc = ioctl(r->fd, USBDEVFS_DISCARDURB, &u->kurb);
int e = errno;
enif_mutex_unlock(r->lock);
// ENOENT/EINVAL here just means it already completed; treat as success.
if (rc < 0 && e != ENOENT && e != EINVAL)
return err_tuple(env, e);
return am_ok;
}
// fileno(handle) -> integer | {:error, :ebadf} (debug aid; verifies no leak)
static ERL_NIF_TERM nif_fileno(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
UsbFd *r;
if (!enif_get_resource(env, argv[0], usb_fd_type, (void **)&r))
return enif_make_badarg(env);
int fd;
enif_mutex_lock(r->lock);
fd = r->closing ? -1 : r->fd;
enif_mutex_unlock(r->lock);
if (fd < 0)
return enif_make_tuple2(env, am_error, am_ebadf);
return enif_make_int(env, fd);
}
// ---- load ----------------------------------------------------------------
static int load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info) {
(void)priv_data;
(void)load_info;
ErlNifResourceFlags tried;
ErlNifResourceTypeInit init = {0};
init.dtor = usb_fd_dtor;
init.stop = usb_fd_stop; // needed for enif_select teardown
usb_fd_type = enif_open_resource_type_x(env, "bodge_usb_fd", &init,
ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER,
&tried);
if (!usb_fd_type)
return -1;
am_ok = mk_atom(env, "ok");
am_error = mk_atom(env, "error");
am_ebadf = mk_atom(env, "ebadf");
return 0;
}
// Re-take the resource type (and re-init atoms) so hot code upgrade works.
static int upgrade(ErlNifEnv *env, void **priv_data, void **old_priv_data,
ERL_NIF_TERM load_info) {
(void)old_priv_data;
return load(env, priv_data, load_info);
}
static ErlNifFunc nif_funcs[] = {
{"open", 2, nif_open, 0},
{"close", 1, nif_close, 0},
{"read", 2, nif_read, 0},
{"write", 2, nif_write, 0},
{"fileno", 1, nif_fileno, 0},
// Blocking usbfs ioctls run on a dirty I/O scheduler; the caller blocks
// on their result while the async engine below serves everyone else.
{"set_interface", 3, nif_set_interface, ERL_NIF_DIRTY_JOB_IO_BOUND},
// claim/release are fast fd bookkeeping ops -> normal scheduler.
{"claim_interface", 2, nif_claim_interface, 0},
{"release_interface", 2, nif_release_interface, 0},
// driver query is fast; detach/attach drive probe/disconnect -> dirty I/O.
{"get_driver", 2, nif_get_driver, 0},
{"detach_driver", 2, nif_detach_driver, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"attach_driver", 2, nif_attach_driver, ERL_NIF_DIRTY_JOB_IO_BOUND},
// recovery: clear stall / reset the device (both drive device round-trips).
{"clear_halt", 2, nif_clear_halt, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"reset", 1, nif_reset, ERL_NIF_DIRTY_JOB_IO_BOUND},
// async engine: all non-blocking, run inline on a normal scheduler.
{"submit_urb", 6, nif_submit, 0},
{"submit_control", 7, nif_submit_control, 0},
{"submit_iso", 5, nif_submit_iso, 0},
{"select", 2, nif_select, 0},
{"select_read", 2, nif_select_read, 0},
{"reap", 1, nif_reap, 0},
{"discard", 2, nif_discard, 0},
// hotplug: netlink uevent socket (read/1 + select_read/2 drive it).
{"netlink_uevent_open", 0, nif_netlink_uevent_open, 0},
{"netlink_read", 2, nif_netlink_read, 0},
};
ERL_NIF_INIT(Elixir.BodgeUSB.Nif, nif_funcs, load, NULL, upgrade, NULL)