Conversation
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: 54108c7 ("fix hotplug") Signed-off-by: Daniel Golle <daniel@makrotopia.org>
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 <daniel@makrotopia.org>
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 <daniel@makrotopia.org>
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 <daniel@makrotopia.org>
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 <daniel@makrotopia.org>
dangowrt
force-pushed
the
procd-modular-hotplug
branch
from
September 25, 2026 23:20
ed4f02a to
68449eb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
/etc/hotplug.jsonis a single file, so a package that needs device-node policy of its own has to edit the file it shares with every other package, or fall back to a shell script under/etc/hotplug.d/. This series lets procd run a directory of rule fragments alongside the main file, and lets it notice when that directory changes.The interpreter already had everything needed on the libubox side:
handle_filemay return a chain of files linked through::next, and__json_script_run()walks the chain, a contract documented since the original json_script commit. No caller ever built such a chain, so procd'shandle_fileis where the work belongs. With this in, an[ "include", "/etc/hotplug.json.d/*.json" ]inhotplug.jsonruns every fragment in sorted glob order, and a package can own its own rules by installing one file.The commits
rule_handle_file()leaked on every rule file it parsed.rule_load_file()out ofrule_handle_file()and builds the::nextchain fromglob(3)when the requested name carries a wildcard. The chain head keeps the pattern as its avl key, becausejson_script_get_file()caches by the requested name and ignores a failedavl_insert(); keying the head by its own path would reparse and leak every fragment on every uevent.hotplugubus object with areloadmethod that drops the parsed-file cache, as a manual and debug entry point.hotplug-dispatch.chand-rolled for/etc/hotplug.dintoutils.c, behindinotify_watch_add(path, mask, callback), and converts that watch to it. The/etc/hotplug.dwatch keeps its mask and itsIN_ISDIRfiltering.Semantics worth knowing when writing a fragment
[ "return" ]inside a fragment ends that fragment and nothing more.handle_include()runs the chain through__json_script_run()and returns 0 whatever the chain did, so the parent file carries on afterwards, and so does the next fragment in the chain. First-match-wins therefore rests on the parent's catch-allmakedevfailing withEEXISTonce a fragment has already created the node, which is exactly how the shipped^sndand^ttyrules have always behaved.The watched directory is derived from the rule file:
/etc/hotplug.jsonis paired with/etc/hotplug.json.d, and the preinit instance started asprocd -h /etc/hotplug-preinit.jsongets/etc/hotplug-preinit.json.d. The watch is set up inhotplug(), which is the one path both instances share; the preinit instance runs without ubus, so it can only be reached there. A directory that does not exist at watch time is reported once and procd carries on.The mask is
IN_CREATE | IN_CLOSE_WRITE | IN_MOVED_TO | IN_DELETE | IN_MOVED_FROM | IN_DELETE_SELF | IN_MOVE_SELF.IN_MOVED_TOis the one a package manager needs, since apk writes a temporary name andrenameat()s it into place;IN_CLOSE_WRITEcovers an edit or a shell redirect;IN_DELETEandIN_MOVED_FROMcover removal, which a post-install hook structurally cannot.IN_MODIFYis absent on purpose: it fires perwrite()and would drop the cache in the middle of a fragment being written.Dropping the cache is
json_script_free()followed byjson_script_init(), and json_script parses a file on first use, so a package installing several fragments at once costs several cheap cache drops and exactly one reparse at the next uevent. No coalescing timer is involved.Testing
Built with procd's own
-Os -Wall -Werror --std=gnu99 -Wmissing-declarations, for x86_64 musl, all targets.The rule semantics were exercised in a harness linking the real
json_script.cagainst a copy ofrule_handle_file(), fed the shippedhotplug.jsonwith an include added and three fragments in the drop-in directory: a claimed node lands in its fragment's group and the parent's catch-all then fails harmlessly, an unclaimed node gets only the catch-all,dri/card0anddri/renderD128land in different groups,nullstill short-circuits, andremoveis unaffected. The first uevent parses four files and every later one parses none, which is the chain-caching contract above. Repeated free/init cycles with fragments added and removed in between are clean undervalgrind --leak-check=full --error-exitcode=9.The runtime behaviour was checked against the built binary in a user and network namespace, driving real uevents over
NETLINK_KOBJECT_UEVENTand real filesystem operations on the drop-in directory./proc/<pid>/fdinfoconfirms the watch sits on the expected inode with mask0xfc8, for/etc/hotplug.json.dand for/etc/hotplug-preinit.json.dalike. A fragment renamed in, deleted, edited in place, renamed out, renamed back, and several dropped at once each take effect on the next uevent, and removing the drop-in directory entirely leaves only the parent's rules. Against a private ubusd, the refactored/etc/hotplug.dwatch still registers and unregistershotplug.<subsystem>as subsystem directories are created, deleted, renamed in and renamed out.