diff --git a/Makefile b/Makefile index 427e6d85..6e1042ec 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,11 @@ NPROC := $(shell nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || ech # Verbose control - V=1 shows full commands, V=0 (default) shows kernel-style short logs V ?= 0 + +# Optional private control bridge for the external C++ droidspaces-socketd. +# Keep this off by default so the stock static Droidspaces binary stays unchanged. +ENABLE_SOCKETD_BACKEND ?= 0 + ifeq ($(V),1) Q = msg_cc = @@ -58,6 +63,11 @@ CFLAGS += -fstack-protector-strong LDFLAGS = -static -no-pie -flto=auto -pthread LIBS = -lutil +ifeq ($(ENABLE_SOCKETD_BACKEND),1) + CFLAGS += -DDS_ENABLE_SOCKETD_BACKEND=1 + SRCS += $(SRC_DIR)/socketd_bridge.c +endif + # Auto-detect architecture from compiler ARCH := $(shell $(CC) -dumpmachine 2>/dev/null | cut -d'-' -f1 | \ sed 's/x86_64/x86_64/; s/aarch64/aarch64/; s/i686/x86/; \ @@ -104,6 +114,8 @@ help: @echo "" @echo "Options:" @echo " V=1 - Show full compiler commands" + @echo " ENABLE_SOCKETD_BACKEND=1" + @echo " - Compile the private droidspaces-socketd backend bridge" @echo "" @echo "Other:" @echo " make clean - Remove build artifacts" diff --git a/src/boot.c b/src/boot.c index 5e2ef245..65fcdc34 100644 --- a/src/boot.c +++ b/src/boot.c @@ -187,8 +187,9 @@ int internal_boot(struct ds_config *cfg) { return -1; } - /* Detect init system once - used for seccomp and cgroup setup */ - int is_systemd = is_systemd_rootfs(cfg->rootfs_path); + /* Init family was classified before fork in start_rootfs(). + * Boot-time setup only needs to know whether that family is systemd. */ + int is_systemd = (cfg->init_type == DS_INIT_SYSTEMD); /* 3. Setup volatile overlay INSIDE the container's mount namespace. * This MUST happen here (not in parent) so the overlay's connection to diff --git a/src/container.c b/src/container.c index 68f5e729..692111c8 100644 --- a/src/container.c +++ b/src/container.c @@ -224,6 +224,7 @@ static void cleanup_container_resources(struct ds_config *cfg, pid_t pid, * so start_rootfs() can detect the existing mount and reuse it. */ if (!skip_unmount) { remove_mount_path(cfg->pidfile); + remove_init_type(cfg->pidfile); if (cfg->pidfile[0]) unlink(cfg->pidfile); if (global_pidfile[0] && strcmp(cfg->pidfile, global_pidfile) != 0) @@ -436,6 +437,11 @@ int start_rootfs(struct ds_config *cfg) { unmount_rootfs_img(cfg->img_mount_point, cfg->foreground); return -1; } + + /* Classify the container init family while the normalized host rootfs + * path is already in scope. Detecting here avoids rebuilding the same + * probe path later solely for shutdown metadata. */ + cfg->init_type = detect_container_init(rootfs_norm); } /* 2b. Android Termux Bridge Preparation - only if flag is set */ @@ -1136,6 +1142,9 @@ int start_rootfs(struct ds_config *cfg) { if (cfg->is_img_mount) save_mount_path(cfg->pidfile, cfg->img_mount_point); + /* Also save init type */ + save_init_type(cfg->pidfile, cfg->init_type); + /* 11. Foreground or background finish */ if (cfg->foreground) { @@ -1261,8 +1270,13 @@ int stop_rootfs(struct ds_config *cfg, int skip_unmount) { ds_init_type_t init_type = DS_INIT_UNKNOWN; const char *probe_root = cfg->img_mount_point[0] ? cfg->img_mount_point : cfg->rootfs_path; - if (probe_root[0]) - init_type = detect_container_init(probe_root); + if (__builtin_expect( (read_init_type(cfg->pidfile, &init_type) != 0 || + init_type == DS_INIT_UNKNOWN), 0)) { + /* Fallback for containers launched before .init sidecars existed, + * or if runtime metadata was lost / non-informative. */ + if (__builtin_expect(probe_root[0], '/')) + init_type = detect_container_init(probe_root); + } switch (init_type) { case DS_INIT_PROCD: diff --git a/src/daemon.c b/src/daemon.c index 7bdaf0ca..bb4813a8 100644 --- a/src/daemon.c +++ b/src/daemon.c @@ -22,6 +22,10 @@ */ #include "droidspace.h" +#ifdef DS_ENABLE_SOCKETD_BACKEND +#include "socketd_bridge.h" +#endif + #include #include #include @@ -815,6 +819,11 @@ int ds_daemon_run(int foreground, char **argv) { /* SIGUSR2: app sends this after a live binary swap as an acknowledgment */ signal(SIGUSR2, sigusr2_handler); +#ifdef DS_ENABLE_SOCKETD_BACKEND + if (ds_socketd_bridge_start() < 0) + ds_warn("Failed to start droidspaces-socketd backend bridge: %s", strerror(errno)); +#endif + /* Write PID file so the Android app can signal us */ { char pid_path[PATH_MAX]; diff --git a/src/droidspace.h b/src/droidspace.h index 30fdfe01..e33bf929 100644 --- a/src/droidspace.h +++ b/src/droidspace.h @@ -128,6 +128,7 @@ #define DS_EXT_PID ".pid" #define DS_EXT_MOUNT ".mount" #define DS_EXT_LOCK ".lock" +#define DS_EXT_INIT ".init" /* Signals */ #define DS_SIG_STOP (SIGRTMIN + 3) @@ -271,6 +272,17 @@ struct ds_port_forward { */ #define DS_PRIV_FULL (0xFF) /* All above */ +typedef enum { + DS_INIT_UNKNOWN = 0, + DS_INIT_SYSTEMD, /* SIGRTMIN+3 */ + DS_INIT_PROCD, /* SIGUSR2 -- OpenWrt; SIGTERM = reboot there! */ + DS_INIT_OPENRC, /* SIGTERM */ + DS_INIT_RUNIT, /* SIGCONT */ + DS_INIT_S6, /* SIGUSR2 */ + DS_INIT_BUSYBOX, /* SIGUSR2 */ + DS_INIT_SYSVINIT, /* SIGTERM */ +} ds_init_type_t; + struct ds_config { /* Paths */ char rootfs_path[PATH_MAX]; /* --rootfs= */ @@ -309,6 +321,7 @@ struct ds_config { pid_t intermediate_pid; /* intermediate fork pid */ int is_img_mount; /* 1 if rootfs was loop-mounted from .img */ char img_mount_point[PATH_MAX]; /* where the .img was mounted */ + ds_init_type_t init_type; /* detected container PID 1 init family */ /* NAT networking synchronization pipes * Both pairs are initialised to {-1,-1} in main() after memset. @@ -386,6 +399,9 @@ int read_and_validate_pid(const char *pidfile, pid_t *pid_out); int save_mount_path(const char *pidfile, const char *mount_path); int read_mount_path(const char *pidfile, char *buf, size_t size); int remove_mount_path(const char *pidfile); +int save_init_type(const char *pidfile, ds_init_type_t init_type); +int read_init_type(const char *pidfile, ds_init_type_t *init_type_out); +int remove_init_type(const char *pidfile); void firmware_path_add(const char *fw_path); void firmware_path_remove(const char *fw_path); int run_command(char *const argv[]); @@ -399,17 +415,6 @@ void print_ds_banner(void); void print_privileged_warning(int privileged_mask); int is_systemd_rootfs(const char *path); -typedef enum { - DS_INIT_UNKNOWN = 0, - DS_INIT_SYSTEMD, /* SIGRTMIN+3 */ - DS_INIT_PROCD, /* SIGUSR2 -- OpenWrt; SIGTERM = reboot there! */ - DS_INIT_OPENRC, /* SIGTERM */ - DS_INIT_RUNIT, /* SIGCONT */ - DS_INIT_S6, /* SIGUSR2 */ - DS_INIT_BUSYBOX, /* SIGUSR2 */ - DS_INIT_SYSVINIT, /* SIGTERM */ -} ds_init_type_t; - ds_init_type_t detect_container_init(const char *path); int get_user_shell(const char *user, char *shell_buf, size_t size); void check_kernel_recommendation(void); diff --git a/src/pid.c b/src/pid.c index d11fc1db..4b2cee35 100644 --- a/src/pid.c +++ b/src/pid.c @@ -221,6 +221,7 @@ int count_running_containers(char *first_name, size_t size) { /* Explicit pruning during scan */ unlink(tmp_cfg.pidfile); remove_mount_path(tmp_cfg.pidfile); + remove_init_type(tmp_cfg.pidfile); } } } @@ -437,6 +438,7 @@ int show_containers(void) { /* Explicit pruning during scan */ unlink(tmp_cfg.pidfile); remove_mount_path(tmp_cfg.pidfile); + remove_init_type(tmp_cfg.pidfile); } } } @@ -676,6 +678,7 @@ int scan_containers(void) { /* Stale PID file, nuke it */ unlink(pf); remove_mount_path(pf); + remove_init_type(pf); } } closedir(d); diff --git a/src/socketd_bridge.c b/src/socketd_bridge.c new file mode 100644 index 00000000..1a40f0cd --- /dev/null +++ b/src/socketd_bridge.c @@ -0,0 +1,233 @@ +/* + * Droidspaces v6 - private backend bridge for droidspaces-socketd + * + * Copyright (C) 2026 ravindu644 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "droidspace.h" +#include "socketd_bridge.h" +#include "socketd_protocol.h" + +/* + * The public Docker/Podman-compatible socket belongs to the external C++ + * droidspaces-socketd daemon. This bridge is deliberately narrower: it is a + * local, private, privileged control path that will eventually expose the + * Droidspaces-native operations needed by that compatibility daemon. + */ + +static int socketd_read_exact(int fd, void *buf, size_t len) { + uint8_t *p = (uint8_t *)buf; + while (len > 0) { + ssize_t r = read(fd, p, len); + if (r == 0) + return -1; + if (r < 0) { + if (errno == EINTR) + continue; + return -1; + } + p += (size_t)r; + len -= (size_t)r; + } + return 0; +} + +static socklen_t socketd_backend_addr(struct sockaddr_un *addr) { + memset(addr, 0, sizeof(*addr)); + addr->sun_family = AF_UNIX; + + size_t name_len = strlen(DS_SOCKETD_BACKEND_SOCK_NAME); + if (name_len >= sizeof(addr->sun_path)) + name_len = sizeof(addr->sun_path) - 1; + + memcpy(addr->sun_path + 1, DS_SOCKETD_BACKEND_SOCK_NAME, name_len); + return (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 + name_len); +} + +static int socketd_send_response(int fd, enum ds_socketd_status status, + const void *payload, uint32_t payload_len) { + struct ds_socketd_response_header hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.magic_be = htonl(DS_SOCKETD_PROTO_MAGIC); + hdr.version_be = htons(DS_SOCKETD_PROTO_VERSION); + hdr.status_be = htons((uint16_t)status); + hdr.payload_len_be = htonl(payload_len); + + if (write_all(fd, &hdr, sizeof(hdr)) != (ssize_t)sizeof(hdr)) + return -1; + + if (payload_len > 0 && payload != NULL) { + if (write_all(fd, payload, payload_len) != (ssize_t)payload_len) + return -1; + } + return 0; +} + +static int socketd_peer_authorized(int fd) { +#ifdef SO_PEERCRED + struct ucred cred; + socklen_t cred_len = sizeof(cred); + memset(&cred, 0, sizeof(cred)); + + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &cred_len) < 0) + return 0; + if (cred_len != sizeof(cred)) + return 0; + + /* + * The first socketd implementation is expected to run as root beside the + * Droidspaces daemon. Same-EUID access keeps local developer/test launches + * usable on desktop Linux without opening the bridge to arbitrary users. + */ + return cred.uid == 0 || cred.uid == geteuid(); +#else + (void)fd; + return 0; +#endif +} + +static int socketd_discard_payload(int fd, uint32_t len) { + char buf[4096]; + uint32_t remaining = len; + while (remaining > 0) { + size_t chunk = remaining < sizeof(buf) ? (size_t)remaining : sizeof(buf); + if (socketd_read_exact(fd, buf, chunk) < 0) + return -1; + remaining -= (uint32_t)chunk; + } + return 0; +} + +static void socketd_handle_conn(int conn) { + struct ds_socketd_request_header req; + memset(&req, 0, sizeof(req)); + + if (!socketd_peer_authorized(conn)) { + socketd_send_response(conn, DS_SOCKETD_STATUS_FORBIDDEN, NULL, 0); + return; + } + + if (socketd_read_exact(conn, &req, sizeof(req)) < 0) { + socketd_send_response(conn, DS_SOCKETD_STATUS_BAD_REQUEST, NULL, 0); + return; + } + + uint32_t magic = ntohl(req.magic_be); + uint16_t version = ntohs(req.version_be); + uint16_t opcode = ntohs(req.opcode_be); + uint32_t payload_len = ntohl(req.payload_len_be); + + if (magic != DS_SOCKETD_PROTO_MAGIC || version != DS_SOCKETD_PROTO_VERSION) { + socketd_send_response(conn, DS_SOCKETD_STATUS_BAD_REQUEST, NULL, 0); + return; + } + + if (payload_len > DS_SOCKETD_MAX_PAYLOAD) { + socketd_send_response(conn, DS_SOCKETD_STATUS_BAD_REQUEST, NULL, 0); + return; + } + + /* + * The currently implemented opcodes do not consume payloads, but draining + * a well-sized payload keeps the framing strict and future-proofs callers. + */ + if (payload_len > 0 && socketd_discard_payload(conn, payload_len) < 0) { + socketd_send_response(conn, DS_SOCKETD_STATUS_BAD_REQUEST, NULL, 0); + return; + } + + switch ((enum ds_socketd_opcode)opcode) { + case DS_SOCKETD_OP_PING: { + static const char pong[] = "PONG"; + socketd_send_response(conn, DS_SOCKETD_STATUS_OK, pong, + (uint32_t)(sizeof(pong) - 1)); + return; + } + + case DS_SOCKETD_OP_CAPABILITIES: { + uint32_t caps_be = htonl(DS_SOCKETD_CAP_PROTOCOL_V1 | + DS_SOCKETD_CAP_PING | + DS_SOCKETD_CAP_CAPABILITIES); + socketd_send_response(conn, DS_SOCKETD_STATUS_OK, &caps_be, + (uint32_t)sizeof(caps_be)); + return; + } + + case DS_SOCKETD_OP_INFO: + case DS_SOCKETD_OP_LIST_CONTAINERS: + case DS_SOCKETD_OP_INSPECT_CONTAINER: + case DS_SOCKETD_OP_START_CONTAINER: + case DS_SOCKETD_OP_STOP_CONTAINER: + case DS_SOCKETD_OP_RESTART_CONTAINER: + socketd_send_response(conn, DS_SOCKETD_STATUS_UNSUPPORTED, NULL, 0); + return; + + default: + socketd_send_response(conn, DS_SOCKETD_STATUS_UNSUPPORTED, NULL, 0); + return; + } +} + +static int socketd_bridge_loop(void) { + struct sockaddr_un addr; + int server = socket(AF_UNIX, SOCK_STREAM, 0); + if (server < 0) + return -1; + + fcntl(server, F_SETFD, FD_CLOEXEC); + + socklen_t addr_len = socketd_backend_addr(&addr); + if (bind(server, (struct sockaddr *)&addr, addr_len) < 0) { + close(server); + return -1; + } + + if (listen(server, SOMAXCONN) < 0) { + close(server); + return -1; + } + + ds_log("droidspaces-socketd backend bridge listening on @%s", + DS_SOCKETD_BACKEND_SOCK_NAME); + + for (;;) { + int conn = accept(server, NULL, NULL); + if (conn < 0) { + if (errno == EINTR) + continue; + ds_warn("socketd backend accept failed: %s", strerror(errno)); + continue; + } + + fcntl(conn, F_SETFD, FD_CLOEXEC); + socketd_handle_conn(conn); + close(conn); + } + + return 0; +} + +int ds_socketd_bridge_start(void) { + pid_t child = fork(); + if (child < 0) + return -1; + + if (child > 0) { + ds_log("droidspaces-socketd backend bridge process started (PID %d)", child); + return 0; + } + + prctl(PR_SET_NAME, "[ds-socketd]", 0, 0, 0); + signal(SIGPIPE, SIG_IGN); + +#ifdef PR_SET_PDEATHSIG + prctl(PR_SET_PDEATHSIG, SIGTERM); + if (getppid() == 1) + _exit(0); +#endif + + int rc = socketd_bridge_loop(); + ds_error("droidspaces-socketd backend bridge exited: %s", strerror(errno)); + _exit(rc == 0 ? 0 : 1); +} diff --git a/src/socketd_bridge.h b/src/socketd_bridge.h new file mode 100644 index 00000000..192517da --- /dev/null +++ b/src/socketd_bridge.h @@ -0,0 +1,19 @@ +/* + * Droidspaces v6 - private backend bridge for droidspaces-socketd + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef DROIDSPACES_SOCKETD_BRIDGE_H +#define DROIDSPACES_SOCKETD_BRIDGE_H + +#ifdef __cplusplus +extern "C" { +#endif + +int ds_socketd_bridge_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* DROIDSPACES_SOCKETD_BRIDGE_H */ diff --git a/src/socketd_protocol.h b/src/socketd_protocol.h new file mode 100644 index 00000000..6249f71e --- /dev/null +++ b/src/socketd_protocol.h @@ -0,0 +1,86 @@ +/* + * Droidspaces v6 - private socketd backend wire protocol + * SPDX-License-Identifier: GPL-3.0-or-later + * + * This header is deliberately C/C++ compatible. The existing Droidspaces + * daemon consumes it from C; the future droidspaces-socketd daemon will use it + * from C++ without pulling C++ types into the native runtime. + */ + +#ifndef DROIDSPACES_SOCKETD_PROTOCOL_H +#define DROIDSPACES_SOCKETD_PROTOCOL_H + +#include + +#if defined(__GNUC__) +#define DS_SOCKETD_PACKED __attribute__((packed)) +#else +#define DS_SOCKETD_PACKED +#endif + +#define DS_SOCKETD_BACKEND_SOCK_NAME "droidspaces-socketd-backend" + +/* ASCII "DSAP" in network byte order after htonl(). */ +#define DS_SOCKETD_PROTO_MAGIC 0x44534150u +#define DS_SOCKETD_PROTO_VERSION 1u +#define DS_SOCKETD_MAX_PAYLOAD (1024u * 1024u) + +enum ds_socketd_opcode { + DS_SOCKETD_OP_PING = 1, + DS_SOCKETD_OP_CAPABILITIES = 2, + DS_SOCKETD_OP_INFO = 3, + DS_SOCKETD_OP_LIST_CONTAINERS = 4, + DS_SOCKETD_OP_INSPECT_CONTAINER = 5, + DS_SOCKETD_OP_START_CONTAINER = 6, + DS_SOCKETD_OP_STOP_CONTAINER = 7, + DS_SOCKETD_OP_RESTART_CONTAINER = 8, +}; + +enum ds_socketd_status { + DS_SOCKETD_STATUS_OK = 0, + DS_SOCKETD_STATUS_BAD_REQUEST = 1, + DS_SOCKETD_STATUS_UNSUPPORTED = 2, + DS_SOCKETD_STATUS_NOT_FOUND = 3, + DS_SOCKETD_STATUS_INTERNAL_ERROR = 4, + DS_SOCKETD_STATUS_FORBIDDEN = 5, +}; + +enum ds_socketd_capability { + DS_SOCKETD_CAP_PROTOCOL_V1 = 1u << 0, + DS_SOCKETD_CAP_PING = 1u << 1, + DS_SOCKETD_CAP_CAPABILITIES = 1u << 2, + DS_SOCKETD_CAP_INFO = 1u << 3, + DS_SOCKETD_CAP_LIST_CONTAINERS = 1u << 4, + DS_SOCKETD_CAP_INSPECT_CONTAINER = 1u << 5, + DS_SOCKETD_CAP_LIFECYCLE = 1u << 6, +}; + +/* + * Request frame: + * magic_be DS_SOCKETD_PROTO_MAGIC via htonl() + * version_be DS_SOCKETD_PROTO_VERSION via htons() + * opcode_be enum ds_socketd_opcode via htons() + * payload_len_be number of payload bytes via htonl() + */ +struct DS_SOCKETD_PACKED ds_socketd_request_header { + uint32_t magic_be; + uint16_t version_be; + uint16_t opcode_be; + uint32_t payload_len_be; +}; + +/* + * Response frame: + * magic_be DS_SOCKETD_PROTO_MAGIC via htonl() + * version_be DS_SOCKETD_PROTO_VERSION via htons() + * status_be enum ds_socketd_status via htons() + * payload_len_be number of payload bytes via htonl() + */ +struct DS_SOCKETD_PACKED ds_socketd_response_header { + uint32_t magic_be; + uint16_t version_be; + uint16_t status_be; + uint32_t payload_len_be; +}; + +#endif /* DROIDSPACES_SOCKETD_PROTOCOL_H */ diff --git a/src/utils.c b/src/utils.c index 393b4cd9..75c1b89f 100644 --- a/src/utils.c +++ b/src/utils.c @@ -714,6 +714,95 @@ int remove_mount_path(const char *pidfile) { return unlink(mpath); } +/* --------------------------------------------------------------------------- + * Init-type sidecar files (.init) + * ---------------------------------------------------------------------------*/ + +static void pidfile_to_initfile(const char *pidfile, char *buf, size_t size) { + safe_strncpy(buf, pidfile, size); + char *dot = strrchr(buf, '.'); + if (dot && strcmp(dot, DS_EXT_PID) == 0) { + /* If it ends in .pid, replace it */ + snprintf(dot, size - (size_t)(dot - buf), DS_EXT_INIT); + } else { + /* Otherwise just append */ + strncat(buf, DS_EXT_INIT, size - strlen(buf) - 1); + } +} + +static const char *init_type_to_string(ds_init_type_t type) { + switch (type) { + case DS_INIT_SYSTEMD: + return "systemd"; + case DS_INIT_PROCD: + return "procd"; + case DS_INIT_OPENRC: + return "openrc"; + case DS_INIT_RUNIT: + return "runit"; + case DS_INIT_S6: + return "s6"; + case DS_INIT_BUSYBOX: + return "busybox"; + case DS_INIT_SYSVINIT: + return "sysvinit"; + case DS_INIT_UNKNOWN: + default: + return "unknown"; + } +} + +static ds_init_type_t init_type_from_string(const char *s) { + if (!s || s[0] == '\0') + return DS_INIT_UNKNOWN; + + if (strcmp(s, "systemd") == 0) + return DS_INIT_SYSTEMD; + if (strcmp(s, "procd") == 0) + return DS_INIT_PROCD; + if (strcmp(s, "openrc") == 0) + return DS_INIT_OPENRC; + if (strcmp(s, "runit") == 0) + return DS_INIT_RUNIT; + if (strcmp(s, "s6") == 0) + return DS_INIT_S6; + if (strcmp(s, "busybox") == 0) + return DS_INIT_BUSYBOX; + if (strcmp(s, "sysvinit") == 0) + return DS_INIT_SYSVINIT; + + return DS_INIT_UNKNOWN; +} + +int save_init_type(const char *pidfile, ds_init_type_t init_type) { + char ipath[PATH_MAX]; + pidfile_to_initfile(pidfile, ipath, sizeof(ipath)); + return write_file(ipath, init_type_to_string(init_type)); +} + +int read_init_type(const char *pidfile, ds_init_type_t *init_type_out) { + if (!init_type_out) + return -1; + + char ipath[PATH_MAX]; + char buf[64]; + + pidfile_to_initfile(pidfile, ipath, sizeof(ipath)); + + if (read_file(ipath, buf, sizeof(buf)) < 0) + return -1; + + buf[strcspn(buf, "\r\n")] = '\0'; + *init_type_out = init_type_from_string(buf); + return 0; +} + +int remove_init_type(const char *pidfile) { + char ipath[PATH_MAX]; + pidfile_to_initfile(pidfile, ipath, sizeof(ipath)); + return unlink(ipath); +} + /* --------------------------------------------------------------------------- * Kernel firmware search path management *