The bootloader has a preload feature that allows a kernel module to be allocated by the bootloader in low memory before the kernel is started. A pointer to the module is given to the kernel via a bootinfo, and the kernel parses it and uses it as-is in low memory. There is no granularity provided by this process.
The first patch [1] forces the kernel to reallocate the preloaded modules into dynamic memory. We can then map this area [2] read-only on amd64 and i386. When loading a module from VFS (and now, from the bootloader), the kernel packs up the useful data into one big RWX chunk. The second patch [3] splits this chunk into two different text and data+bss+rodata chunks. The latter is made non-executable. Note that this also provides some kind of ASLR, since the chunks are not necessarily contiguous. It will be easy then to put rodata into another chunk and mprotect the text and rodata so that they get only RX and R. There is still [4], but I'll fix that later. Ok? [1] http://m00nbsd.net/garbage/modules_1.diff [2] https://nxr.netbsd.org/xref/src/sys/arch/amd64/amd64/locore.S#706 [3] http://m00nbsd.net/garbage/modules_2.diff [4] https://nxr.netbsd.org/xref/src/sys/gdbscripts/module#70