From 287c3be58ac156a1c1a48cd49c12509f4189ab5f Mon Sep 17 00:00:00 2001 From: Luke Craig Date: Wed, 8 Jul 2026 06:24:03 -0400 Subject: [PATCH] portal/devfs: share a dynamic char-major block instead of one major per device Devices registered with major < 0 previously each called alloc_chrdev_region(), which burns one whole char-device MAJOR per device. The kernel only has ~234 free majors, so firmware that models many /dev nodes exhausted the pool: e.g. RouterOS opens /dev/log%d in a busy loop and we enumerate /dev/log0..255, so ~105 registrations past the limit failed with HYPER_RESP_WRITE_FAIL ("kernel returned 0"). The guest still booted, but the /dev/logN nodes the firmware actually opens went unmodeled -> ENOENT open spin -> CPU drain, defeating the purpose of modeling them. Reserve majors in blocks and hand out one MINOR per device via igloo_alloc_dynamic_devt(): a single major covers IGLOO_DYN_MINORS (256) devices, and a fresh block is allocated only when the current one fills. This lifts the effective cap from ~234 devices to ~234 * 256. The two cdev_add()/device_create() failure paths now only unregister the region for static-major registrations, since a shared-block minor does not own its region. A prior shared-major attempt (0.0.75, reverted in 0.0.76) was pulled for an unrelated mipsel do_ade unaligned-access crash. That crash was reproduced on a period-accurate image and confirmed to originate from a separate unaligned-access bug in the era's driver/qemu (since fixed on the current base), not from major sharing: 0.0.75's exact allocator ported onto the current base boots clean. This scheme was validated on the real armel RouterOS6 target (105 -> 0 devfs registration failures, verifier all-pass) and across the full MIPS matrix {mipsel,mipseb,mips64el,mips64eb} x {4.10,6.13} (8/8 clean boots, no do_ade). --- src/portal/portal_devfs.c | 54 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/src/portal/portal_devfs.c b/src/portal/portal_devfs.c index 3fbd1ae..fcc3f5c 100644 --- a/src/portal/portal_devfs.c +++ b/src/portal/portal_devfs.c @@ -515,6 +515,50 @@ static void ensure_portal_class(void) } } +/* + * Shared dynamic char-device region. + * + * A device registered with major < 0 wants a kernel-assigned device number. + * The naive approach — alloc_chrdev_region() per device — burns one whole + * MAJOR each time, and the kernel only has ~234 free majors. Firmware that + * models many /dev nodes (e.g. RouterOS opens /dev/log%d and we enumerate + * /dev/log0..255) exhausted that pool, so every device past the limit failed + * to register with "kernel returned 0". + * + * Instead, reserve majors in blocks and hand out one MINOR per device. A + * single major then covers IGLOO_DYN_MINORS devices, and we allocate a fresh + * block only when the current one fills, lifting the effective limit from + * ~234 devices to ~234 * IGLOO_DYN_MINORS. + */ +#define IGLOO_DYN_MINORS 256 +static DEFINE_MUTEX(igloo_dyn_lock); +static dev_t igloo_dyn_base; /* MKDEV(major, 0) of the current block */ +static unsigned int igloo_dyn_count; /* minors reserved in the current block */ +static unsigned int igloo_dyn_used; /* minors handed out from the block */ + +static int igloo_alloc_dynamic_devt(dev_t *out) +{ + int ret = 0; + + mutex_lock(&igloo_dyn_lock); + if (igloo_dyn_used >= igloo_dyn_count) { + dev_t base; + + ret = alloc_chrdev_region(&base, 0, IGLOO_DYN_MINORS, "igloo_devfs"); + if (ret < 0) { + mutex_unlock(&igloo_dyn_lock); + return ret; + } + igloo_dyn_base = base; + igloo_dyn_count = IGLOO_DYN_MINORS; + igloo_dyn_used = 0; + } + *out = MKDEV(MAJOR(igloo_dyn_base), MINOR(igloo_dyn_base) + igloo_dyn_used); + igloo_dyn_used++; + mutex_unlock(&igloo_dyn_lock); + return 0; +} + /* * ------------------------------------------------------------------------- * Directory Logic (New) * ------------------------------------------------------------------------- @@ -734,7 +778,9 @@ void handle_op_devfs_create_device(portal_region *mem_region) bool dynamic_chrdev_region = req->major < 0; if (dynamic_chrdev_region) { - ret = alloc_chrdev_region(&devt, 0, 1, final_device_name); + // Hand out a MINOR from a shared major block rather than burning a + // whole major per device (see igloo_alloc_dynamic_devt). + ret = igloo_alloc_dynamic_devt(&devt); } else { devt = MKDEV(req->major, req->minor); if (req->replace) unregister_chrdev_region(devt, 1); @@ -757,7 +803,9 @@ void handle_op_devfs_create_device(portal_region *mem_region) pe->cdev.owner = THIS_MODULE; if (cdev_add(&pe->cdev, devt, 1)) { - unregister_chrdev_region(devt, 1); + // A shared-region minor belongs to a larger reserved block; only + // standalone (static-major) registrations own their region. + if (!dynamic_chrdev_region) unregister_chrdev_region(devt, 1); goto fail_alloc; } @@ -774,7 +822,7 @@ void handle_op_devfs_create_device(portal_region *mem_region) list_del(&pe->list); spin_unlock(&devfs_entry_lock); cdev_del(&pe->cdev); - unregister_chrdev_region(devt, 1); + if (!dynamic_chrdev_region) unregister_chrdev_region(devt, 1); goto fail_alloc; } }