diff --git a/src/portal/portal_devfs.c b/src/portal/portal_devfs.c index fcc3f5c..e0f9e62 100644 --- a/src/portal/portal_devfs.c +++ b/src/portal/portal_devfs.c @@ -13,6 +13,8 @@ #include // Required for fput() #include // Required for poll_wait()/poll_table_struct/POLL* (issue #77) #include // Required for wait_queue_head_t/wake_up_interruptible (issue #77) +#include // Required for timer_list/timer_setup/mod_timer (periodic poll heartbeat) +#include // Required for msecs_to_jiffies() (periodic poll heartbeat) #include // Required for shmem_kernel_file_setup() #include // Required for kvzalloc/vzalloc() #include @@ -40,11 +42,16 @@ struct portal_devfs_create_req { int support_mmap; int is_block; int logical_block_size; - int major; + int major; int minor; struct igloo_dev_ops ops; int replace; int parent_id; + // periodic poll heartbeat: if > 0, arm a self-rearming kernel timer that + // marks this node readable every poll_interval_ms and wakes poll_wq. Lets a + // poll()/epoll(timeout=-1) main loop advance at a fixed cadence (models a + // hardware heartbeat, e.g. a watchdog device). 0 = disabled. + int poll_interval_ms; }; // Internal: Track created devices @@ -79,6 +86,16 @@ struct portal_devfs_entry { unsigned int (*python_poll)(struct file *, struct poll_table_struct *); ssize_t (*python_write)(struct file *, const char __user *, size_t, loff_t *); + // periodic poll heartbeat: when poll_interval_ms > 0 a self-rearming timer + // sets poll_ready and wakes poll_wq every interval; the poll proxy then + // reports POLLIN once per tick (edge-consumed via atomic_xchg) and "not + // ready" otherwise. Models a device that delivers a periodic hardware event + // (e.g. a watchdog): neither always-ready (which spins an epoll(-1) loop) + // nor never-ready (which deadlocks it), but a fixed cadence. + struct timer_list poll_timer; + atomic_t poll_ready; + unsigned int poll_interval_ms; + char *name; }; // Internal: Track created directories to reconstruct paths @@ -302,6 +319,26 @@ static int igloo_devfs_proxy_mmap(struct file *file, struct vm_area_struct *vma) return ret; } +/* + * periodic poll heartbeat timer: mark the node readable and wake any poller + * parked on poll_wq, then re-arm. Driven by poll_interval_ms; see the poll + * proxy below for how the tick is consumed. timer_setup()/from_timer() landed + * in 4.14; older kernels use setup_timer() with the entry pointer in ->data. + */ +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0) +static void igloo_devfs_poll_timer_fn(struct timer_list *t) +{ + struct portal_devfs_entry *pe = from_timer(pe, t, poll_timer); +#else +static void igloo_devfs_poll_timer_fn(unsigned long data) +{ + struct portal_devfs_entry *pe = (struct portal_devfs_entry *)data; +#endif + atomic_set(&pe->poll_ready, 1); + wake_up_interruptible(&pe->poll_wq); + mod_timer(&pe->poll_timer, jiffies + msecs_to_jiffies(pe->poll_interval_ms)); +} + /* * poll proxy (issue #77): register the opener on the per-device wait queue so a * "not ready" poll can actually block and be woken later, then defer the @@ -324,6 +361,19 @@ static unsigned int igloo_devfs_proxy_poll(struct file *file, struct poll_table_ poll_wait(file, &pe->poll_wq, pt); + if (pe->poll_interval_ms > 0) { + /* Periodic heartbeat node (e.g. a watchdog device). The timer + * sets poll_ready once per interval; report POLLIN for exactly one + * poll() per tick (atomic_xchg consumes it) and "not ready" otherwise, + * so an epoll(timeout=-1) main loop wakes at a fixed cadence and parks + * in between rather than spinning (always-ready) or deadlocking + * (never-ready). Deliberately not POLLOUT: a constant writable mask + * would re-spin a loop that also watches EPOLLOUT. */ + if (atomic_xchg(&pe->poll_ready, 0)) + return POLLIN | POLLRDNORM; + return 0; + } + if (pe->python_poll) return pe->python_poll(file, pt); /* No data-aware poll modeled -> legacy always-ready behavior. */ @@ -683,6 +733,10 @@ void handle_op_devfs_create_device(portal_region *mem_region) pe->python_poll = req->ops.poll; pe->python_write = req->ops.write; init_waitqueue_head(&pe->poll_wq); + // periodic poll heartbeat cadence (0 = disabled); the timer itself is armed + // only on the char-device path (block devices have no poll proxy). + pe->poll_interval_ms = req->poll_interval_ms; + atomic_set(&pe->poll_ready, 0); pe->is_block = req->is_block; pe->mode = req->mode ? req->mode : 0666; @@ -825,6 +879,18 @@ void handle_op_devfs_create_device(portal_region *mem_region) if (!dynamic_chrdev_region) unregister_chrdev_region(devt, 1); goto fail_alloc; } + + // Arm the periodic poll heartbeat now the entry is fully live. Never + // cancelled: devfs entries persist for the module's lifetime (there is + // no per-entry teardown), matching this timer's self-rearming lifetime. + if (pe->poll_interval_ms > 0) { +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0) + timer_setup(&pe->poll_timer, igloo_devfs_poll_timer_fn, 0); +#else + setup_timer(&pe->poll_timer, igloo_devfs_poll_timer_fn, (unsigned long)pe); +#endif + mod_timer(&pe->poll_timer, jiffies + msecs_to_jiffies(pe->poll_interval_ms)); + } } /* Track it */ diff --git a/src/portal/portal_sysctl.c b/src/portal/portal_sysctl.c index ef9d76d..43c3c0d 100644 --- a/src/portal/portal_sysctl.c +++ b/src/portal/portal_sysctl.c @@ -276,7 +276,7 @@ static struct ctl_table *igloo_find_sysctl_leaf(const char *dir_path, const char // is the operation that panics on old kernels (see the guard in // handle_op_sysctl_create_file), so we only ever do it under a parent we know // is real. -static bool igloo_sysctl_dir_exists(const char *dir_path) +static bool __maybe_unused igloo_sysctl_dir_exists(const char *dir_path) { struct igloo_ctl_table_root *sysctl_root; struct igloo_ctl_dir *curr_dir = NULL; @@ -354,6 +354,30 @@ static bool igloo_sysctl_dir_exists(const char *dir_path) return true; } +/* Subtrees of /proc/sys that are separate mounted filesystems, not sysctls + * (binfmt_misc is the canonical example). register_sysctl() cannot create a + * ctl_dir inside these, and on pre-5.0 kernels the failed attempt faults in the + * cleanup path (drop_sysctl_table()->rb_erase() NULL deref). This mirrors the + * Python _NON_SYSCTL_SUBTREES guard in hyperfile/sysctl.py, which normally + * rejects them before they reach us; this is the driver-side backstop. A + * genuinely new *sysctl* directory (e.g. a vendor-specific boot-environment + * directory) is created fine by register_sysctl and must NOT be refused. */ +static bool igloo_sysctl_dir_is_fs_backed(const char *dir_path) +{ + static const char * const fs_backed[] = { "fs/binfmt_misc" }; + size_t i; + + if (!dir_path || !dir_path[0]) + return false; + for (i = 0; i < ARRAY_SIZE(fs_backed); i++) { + size_t n = strlen(fs_backed[i]); + if (strncmp(dir_path, fs_backed[i], n) == 0 && + (dir_path[n] == '\0' || dir_path[n] == '/')) + return true; + } + return false; +} + void handle_op_sysctl_create_file(portal_region *mem_region) { struct portal_sysctl_create_req *req = (struct portal_sysctl_create_req *)PORTAL_DATA(mem_region); @@ -442,12 +466,17 @@ void handle_op_sysctl_create_file(portal_region *mem_region) // when it has to create a new directory hierarchy and an intermediate // component cannot be created -- e.g. anything under the filesystem-backed // /proc/sys/fs/binfmt_misc node, which returns -EROFS and then faults in the - // cleanup path. Refuse to create an entry whose parent directory does not - // already exist instead of handing the kernel a doomed registration. + // cleanup path. That failure mode is specific to filesystem-backed, non-sysctl + // subtrees; a genuinely new *sysctl* directory (vendor-specific dirs, e.g. a + // bootloader environment exposed under /proc/sys, common in embedded + // firmware) is created safely by register_sysctl(). So refuse only the + // filesystem-backed subtrees, not every + // not-yet-existing parent -- the previous broad existence check wrongly + // dropped legitimate vendor sysctls and hung boots that poll for them. // Newer kernels return NULL from register_sysctl() and are handled by the // !entry->header path below, so this guard is scoped to the affected versions. - if (clean_dir[0] && !igloo_sysctl_dir_exists(clean_dir)) { - printk(KERN_WARNING "portal_sysctl: refusing to create '%s/%s': parent sysctl directory does not exist\n", + if (igloo_sysctl_dir_is_fs_backed(clean_dir)) { + printk(KERN_WARNING "portal_sysctl: refusing to create '%s/%s': parent is a filesystem-backed (non-sysctl) subtree\n", clean_dir, clean_name); kfree(entry->data_buffer); kfree(entry);