From daa7bb065de7acd0f7fd0b261a888af6330f5a20 Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Sun, 20 Sep 2026 17:22:00 +0100 Subject: [PATCH 1/5] hotplug: release the JSON object after parsing a rule file rule_handle_file() converts the parsed json-c object into a blobmsg buffer but never drops its reference, so the object tree stays allocated for the lifetime of procd. Only one rule file is ever loaded, so a single object is leaked rather than one per uevent. Fixes: 54108c71df64 ("fix hotplug") Signed-off-by: Daniel Golle --- plug/hotplug.c | 1 + 1 file changed, 1 insertion(+) diff --git a/plug/hotplug.c b/plug/hotplug.c index 432df5ec..3b658ca3 100644 --- a/plug/hotplug.c +++ b/plug/hotplug.c @@ -502,6 +502,7 @@ rule_handle_file(struct json_script_ctx *ctx, const char *name) blob_buf_init(&script, 0); blobmsg_add_json_element(&script, "", obj); + json_object_put(obj); return json_script_file_from_blobmsg(name, blob_data(script.head), blob_len(script.head)); } From d56de8de9e34f3d0227efae08a2ec243450a0bbe Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Sun, 20 Sep 2026 17:22:36 +0100 Subject: [PATCH 2/5] hotplug: support glob patterns in rule file names There is no way for a package to extend the hotplug rules short of editing the single /etc/hotplug.json it shares with everyone else. json_script already caters for this: its handle_file callback may return a chain of files linked through ::next, which the interpreter runs in order, but no caller ever built one. Build that chain from glob(3) whenever the requested name contains a wildcard, so an "include" of /etc/hotplug.json.d/*.json runs every fragment in sorted order. json_script_get_file() caches by the requested name and ignores a failed avl_insert(), so the head of the chain carries the pattern as its key; keying it by its own path would reparse and leak every fragment on every uevent. Signed-off-by: Daniel Golle --- plug/hotplug.c | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/plug/hotplug.c b/plug/hotplug.c index 3b658ca3..dd5862f1 100644 --- a/plug/hotplug.c +++ b/plug/hotplug.c @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -492,11 +493,11 @@ static const char* rule_handle_var(struct json_script_ctx *ctx, const char *name } static struct json_script_file * -rule_handle_file(struct json_script_ctx *ctx, const char *name) +rule_load_file(const char *path, const char *key) { json_object *obj; - obj = json_object_from_file((char*)name); + obj = json_object_from_file(path); if (!obj) return NULL; @@ -504,7 +505,38 @@ rule_handle_file(struct json_script_ctx *ctx, const char *name) blobmsg_add_json_element(&script, "", obj); json_object_put(obj); - return json_script_file_from_blobmsg(name, blob_data(script.head), blob_len(script.head)); + return json_script_file_from_blobmsg(key, blob_data(script.head), blob_len(script.head)); +} + +static struct json_script_file * +rule_handle_file(struct json_script_ctx *ctx, const char *name) +{ + struct json_script_file *head = NULL, *f, **tail; + const char *key = name; + glob_t gl; + size_t i; + + if (!strpbrk(name, "*?[")) + return rule_load_file(name, name); + + if (glob(name, 0, NULL, &gl)) { + globfree(&gl); + return NULL; + } + + tail = &head; + for (i = 0; i < gl.gl_pathc; i++) { + f = rule_load_file(gl.gl_pathv[i], key); + if (!f) + continue; + + key = NULL; + *tail = f; + tail = &f->next; + } + globfree(&gl); + + return head; } static void rule_handle_command(struct json_script_ctx *ctx, const char *name, From 1bf9612088107c728c02dfcca37fa8449a20724f Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Sun, 20 Sep 2026 17:40:24 +0100 Subject: [PATCH 3/5] hotplug: add a ubus method to reload the rule files Parsed rule files are cached for the lifetime of procd, so a package dropping a fragment into /etc/hotplug.json.d only takes effect after a reboot, and procd is PID 1. Add a "hotplug" object with a "reload" method that drops the cache; the files are read again on the next uevent. The object is only registered once hotplug() has set up the rule engine: procd started as anything other than PID 1 skips that, leaving the json_script context uninitialised. Signed-off-by: Daniel Golle --- plug/hotplug.c | 37 +++++++++++++++++++++++++++++++++++++ procd.h | 5 +++++ ubus.c | 1 + 3 files changed, 43 insertions(+) diff --git a/plug/hotplug.c b/plug/hotplug.c index dd5862f1..dcd699ac 100644 --- a/plug/hotplug.c +++ b/plug/hotplug.c @@ -585,6 +585,43 @@ static struct json_script_ctx jctx = { .handle_file = rule_handle_file, }; +static int hotplug_rules_reload(struct ubus_context *ctx, + struct ubus_object *obj, + struct ubus_request_data *req, + const char *method, struct blob_attr *msg) +{ + json_script_free(&jctx); + json_script_init(&jctx); + + return UBUS_STATUS_OK; +} + +static const struct ubus_method hotplug_rules_methods[] = { + UBUS_METHOD_NOARG("reload", hotplug_rules_reload), +}; + +static struct ubus_object_type hotplug_rules_object_type = + UBUS_OBJECT_TYPE("hotplug", hotplug_rules_methods); + +static struct ubus_object hotplug_rules_object = { + .name = "hotplug", + .type = &hotplug_rules_object_type, + .methods = hotplug_rules_methods, + .n_methods = ARRAY_SIZE(hotplug_rules_methods), +}; + +void ubus_init_hotplug_rules(struct ubus_context *ctx) +{ + int ret; + + if (!rule_file) + return; + + ret = ubus_add_object(ctx, &hotplug_rules_object); + if (ret) + ERROR("Failed to add object: %s\n", ubus_strerror(ret)); +} + static void hotplug_handler_debug(struct blob_attr *data) { char *str; diff --git a/procd.h b/procd.h index 404b51ce..9edf311a 100644 --- a/procd.h +++ b/procd.h @@ -39,10 +39,15 @@ void ubus_init_service(struct ubus_context *ctx); void ubus_init_system(struct ubus_context *ctx); #ifndef DISABLE_INIT void hotplug_ubus_event(struct blob_attr *data); +void ubus_init_hotplug_rules(struct ubus_context *ctx); #else static inline void hotplug_ubus_event(struct blob_attr *data) { } + +static inline void ubus_init_hotplug_rules(struct ubus_context *ctx) +{ +} #endif void procd_state_next(void); diff --git a/ubus.c b/ubus.c index 23f640ab..336fb293 100644 --- a/ubus.c +++ b/ubus.c @@ -77,6 +77,7 @@ ubus_connect_cb(struct uloop_timeout *timeout) udebug_ubus_init(&udebug, ctx, "procd", procd_udebug_cb); ctx->connection_lost = ubus_disconnect_cb; ubus_init_hotplug(ctx); + ubus_init_hotplug_rules(ctx); ubus_init_service(ctx); ubus_init_system(ctx); watch_ubus(ctx); From 056849ce26ad123411513476cbb64bd87475d157 Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Thu, 24 Sep 2026 23:47:15 +0100 Subject: [PATCH 4/5] utils: add an inotify watch helper hotplug-dispatch.c hand-rolls the only inotify user in procd: an inotify fd on a uloop_fd, a read buffer sized for one maximal event, and a loop walking the events of a single read. Any second watch in procd would have to repeat all of it. Move that plumbing into utils.c behind inotify_watch_add(), which takes a path, a mask and a per-event callback, and let all watches share one read buffer. Reads happen inside the uloop callback, so two watches can never use the buffer at the same time. The /etc/hotplug.d watch keeps its mask and its IN_ISDIR filtering. Signed-off-by: Daniel Golle --- hotplug-dispatch.c | 67 ++++++++++++---------------------------------- utils/utils.c | 53 ++++++++++++++++++++++++++++++++++++ utils/utils.h | 16 +++++++++++ 3 files changed, 86 insertions(+), 50 deletions(-) diff --git a/hotplug-dispatch.c b/hotplug-dispatch.c index a67ef2d5..d3d68d71 100644 --- a/hotplug-dispatch.c +++ b/hotplug-dispatch.c @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -39,11 +38,8 @@ #define HOTPLUG_BASEDIR "/etc/hotplug.d" #define HOTPLUG_OBJECT_PREFIX "hotplug." -#define INOTIFY_SZ (sizeof(struct inotify_event) + PATH_MAX + 1) - struct ubus_context *ctx; -static char *inotify_buffer; -static struct uloop_fd fd_inotify_read; +static struct inotify_watch basedir_watch; static AVL_TREE(subsystems, avl_strcmp, false, NULL); @@ -423,41 +419,24 @@ static int init_subsystems(void) return 0; } -static void inotify_read_handler(struct uloop_fd *u, unsigned int events) +static void basedir_event(struct inotify_watch *w, struct inotify_event *in) { - int rc; - char *p; - struct inotify_event *in; - - /* read inotify events */ - while ((rc = read(u->fd, inotify_buffer, INOTIFY_SZ)) == -1 && errno == EINTR); - - if (rc <= 0) + /* skip everything but directories */ + if (!(in->mask & IN_ISDIR)) return; - /* process events from buffer */ - for (p = inotify_buffer; - rc - (p - inotify_buffer) >= (int)sizeof(struct inotify_event); - p += sizeof(struct inotify_event) + in->len) { - in = (struct inotify_event*)p; - - /* skip everything but directories */ - if (!(in->mask & IN_ISDIR)) - continue; - - if (in->len < 1) - continue; + if (in->len < 1) + return; - /* skip hidden files */ - if (in->name[0] == '.') - continue; + /* skip hidden files */ + if (in->name[0] == '.') + return; - /* add/remove subsystem objects */ - if (in->mask & (IN_CREATE | IN_MOVED_TO)) - add_subsystem(in->name); - else if (in->mask & (IN_DELETE | IN_MOVED_FROM)) - remove_subsystem(in->name); - } + /* add/remove subsystem objects */ + if (in->mask & (IN_CREATE | IN_MOVED_TO)) + add_subsystem(in->name); + else if (in->mask & (IN_DELETE | IN_MOVED_FROM)) + remove_subsystem(in->name); } void hotplug_ubus_event(struct blob_attr *data) @@ -488,20 +467,8 @@ void ubus_init_hotplug(struct ubus_context *newctx) printf("failed to initialize hotplug subsystems from %s\n", HOTPLUG_BASEDIR); return; } - fd_inotify_read.fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); - fd_inotify_read.cb = inotify_read_handler; - if (fd_inotify_read.fd == -1) { + if (inotify_watch_add(&basedir_watch, HOTPLUG_BASEDIR, + IN_CREATE | IN_MOVED_TO | IN_DELETE | + IN_MOVED_FROM | IN_ONLYDIR, basedir_event)) printf("failed to initialize inotify handler for %s\n", HOTPLUG_BASEDIR); - return; - } - - inotify_buffer = calloc(1, INOTIFY_SZ); - if (!inotify_buffer) - return; - - if (inotify_add_watch(fd_inotify_read.fd, HOTPLUG_BASEDIR, - IN_CREATE | IN_MOVED_TO | IN_DELETE | IN_MOVED_FROM | IN_ONLYDIR) == -1) - return; - - uloop_fd_add(&fd_inotify_read, ULOOP_READ); } diff --git a/utils/utils.c b/utils/utils.c index ba142699..8f1c4986 100644 --- a/utils/utils.c +++ b/utils/utils.c @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include #include @@ -30,6 +32,10 @@ #define O_PATH 010000000 #endif +#define INOTIFY_SZ (sizeof(struct inotify_event) + PATH_MAX + 1) + +static char *inotify_buffer; + void __blobmsg_list_init(struct blobmsg_list *list, int offset, int len, blobmsg_list_cmp cmp) { @@ -250,3 +256,50 @@ int patch_stdio(const char *device) return rv; } + +static void inotify_watch_handler(struct uloop_fd *ufd, unsigned int events) +{ + struct inotify_watch *w = container_of(ufd, struct inotify_watch, ufd); + struct inotify_event *in; + char *p; + int rc; + + while ((rc = read(ufd->fd, inotify_buffer, INOTIFY_SZ)) == -1 && + errno == EINTR); + + if (rc <= 0) + return; + + for (p = inotify_buffer; + rc - (p - inotify_buffer) >= (int)sizeof(struct inotify_event); + p += sizeof(struct inotify_event) + in->len) { + in = (struct inotify_event *)p; + w->cb(w, in); + } +} + +int inotify_watch_add(struct inotify_watch *w, const char *path, + uint32_t mask, inotify_watch_cb cb) +{ + if (!inotify_buffer) { + inotify_buffer = calloc(1, INOTIFY_SZ); + if (!inotify_buffer) + return -1; + } + + w->ufd.fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (w->ufd.fd == -1) + return -1; + + w->ufd.cb = inotify_watch_handler; + w->cb = cb; + + if (inotify_add_watch(w->ufd.fd, path, mask) == -1 || + uloop_fd_add(&w->ufd, ULOOP_READ)) { + close(w->ufd.fd); + w->ufd.fd = -1; + return -1; + } + + return 0; +} diff --git a/utils/utils.h b/utils/utils.h index d0c621e7..883c9e5d 100644 --- a/utils/utils.h +++ b/utils/utils.h @@ -15,9 +15,12 @@ #ifndef __PROCD_UTILS_H #define __PROCD_UTILS_H +#include + #include #include #include +#include #define CMDLINE_SIZE 2048 @@ -60,4 +63,17 @@ char *get_active_console(char *out, int len); int patch_fd(const char *device, int fd, int flags); int patch_stdio(const char *device); +struct inotify_watch; + +typedef void (*inotify_watch_cb)(struct inotify_watch *w, + struct inotify_event *ev); + +struct inotify_watch { + struct uloop_fd ufd; + inotify_watch_cb cb; +}; + +int inotify_watch_add(struct inotify_watch *w, const char *path, + uint32_t mask, inotify_watch_cb cb); + #endif From 68449eb9156f7ab43bb8e311fef615dd0906c539 Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Thu, 24 Sep 2026 23:47:40 +0100 Subject: [PATCH 5/5] hotplug: reload rules when the drop-in directory changes Parsed rule files are cached for the lifetime of procd, so a fragment added to or removed from the drop-in directory only takes effect after a reboot. Watch the directory that sits next to the rule file, /etc/hotplug.json.d for the instance procd starts itself and /etc/hotplug-preinit.json.d for the one procd -h runs during preinit, and drop the cache on every event. The mask covers a fragment appearing, being replaced by a rename, being altered in place and being removed. hotplug() is the one setup path both instances share: the preinit instance runs without ubus, so a watch placed on the ubus connect path would leave it out. json_script parses a rule file on first use, so dropping the cache costs nothing until the next uevent arrives, however many fragments a package installs at once. Signed-off-by: Daniel Golle --- plug/hotplug.c | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/plug/hotplug.c b/plug/hotplug.c index dcd699ac..70efeccd 100644 --- a/plug/hotplug.c +++ b/plug/hotplug.c @@ -12,6 +12,7 @@ * GNU General Public License for more details. */ +#include #include #include #include @@ -35,11 +36,16 @@ #include #include "../procd.h" +#include "../utils/utils.h" #include "hotplug.h" #define HOTPLUG_WAIT 500 +#define HOTPLUG_RULES_EVENTS (IN_CREATE | IN_CLOSE_WRITE | IN_MOVED_TO | \ + IN_DELETE | IN_MOVED_FROM | IN_DELETE_SELF | \ + IN_MOVE_SELF) + struct cmd_handler; struct cmd_queue { struct list_head list; @@ -585,13 +591,20 @@ static struct json_script_ctx jctx = { .handle_file = rule_handle_file, }; +static struct inotify_watch rules_watch; + +static void hotplug_rules_flush(void) +{ + json_script_free(&jctx); + json_script_init(&jctx); +} + static int hotplug_rules_reload(struct ubus_context *ctx, struct ubus_object *obj, struct ubus_request_data *req, const char *method, struct blob_attr *msg) { - json_script_free(&jctx); - json_script_init(&jctx); + hotplug_rules_flush(); return UBUS_STATUS_OK; } @@ -622,6 +635,29 @@ void ubus_init_hotplug_rules(struct ubus_context *ctx) ERROR("Failed to add object: %s\n", ubus_strerror(ret)); } +static void hotplug_rules_changed(struct inotify_watch *w, + struct inotify_event *ev) +{ + hotplug_rules_flush(); +} + +static void hotplug_rules_watch(const char *rules) +{ + char *dir; + + dir = malloc(strlen(rules) + sizeof(".d")); + if (!dir) + return; + + sprintf(dir, "%s.d", rules); + + if (inotify_watch_add(&rules_watch, dir, HOTPLUG_RULES_EVENTS, + hotplug_rules_changed)) + ERROR("Failed to watch %s: %m\n", dir); + + free(dir); +} + static void hotplug_handler_debug(struct blob_attr *data) { char *str; @@ -696,6 +732,7 @@ void hotplug(char *rules) ERROR("Failed to resize receive buffer: %m\n"); json_script_init(&jctx); + hotplug_rules_watch(rules); queue_proc.cb = queue_proc_cb; uloop_fd_add(&hotplug_fd, ULOOP_READ); }