Subject: [PATCH] initrd: Parse kernel command line and pass module parameters

From: nathan <[email-masked]>
Date: Sat, 30 Dec 2023 23:14:10 -0500

GUIX's initrd code ignores the kernel command line and always passes no parameters to the modules it loads.
This fixes it with command line parsing code that is accurate to what modprobe does.
The user's module options, like somemodule.param=val, are properly passed now.
It also allows the modprobe.blacklist option to optionally be quoted and normalizes the module names given in the blacklist.
modprobe.blacklist="module1,mod-ule2" -> '("module1" "mod_ule2")

I tested passing module options with the command line in a VM and on my real system.
Tested blacklist code in a VM.

I've also attached my code I used to test the commandline parsing function manually.

fix bug#55907
https://lists.gnu.org/archive/html/bug-guix/2022-06/msg00157.html

>From 626930cbad11e7f5546589fc290b2c95fee97b80 Mon Sep 17 00:00:00 2001
Message-ID: <626[email-masked]>
From: nathan <[email-masked]>
Date: Sat, 30 Dec 2023 22:10:26 -0500
Subject: [PATCH 1/3] linux-modules: Allow parameters to be passed to kernel
 modules

* gnu/build/linux-modules.scm (load-linux-module*): New parameter,
  `get-options', for getting kernel module parameters to give to Linux.

Change-Id: I8c4629a1404270fe641fef02a9fbbaeac4360e65
---
 gnu/build/linux-modules.scm | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/gnu/build/linux-modules.scm b/gnu/build/linux-modules.scm
index 12cb9c4ba6..c4950ea6d2 100644
--- a/gnu/build/linux-modules.scm
+++ b/gnu/build/linux-modules.scm
@@ -332,12 +332,15 @@ (define* (load-linux-module* file
                              #:key
                              (recursive? #t)
                              (lookup-module dot-ko)
-                             (black-list (module-black-list)))
+                             (black-list (module-black-list))
+                             (get-options (const "")))
   "Load Linux module from FILE, the name of a '.ko[.gz|.xz]' file; return true
 on success, false otherwise.  When RECURSIVE? is true, load its dependencies
 first (à la 'modprobe'.)  The actual files containing modules depended on are
 obtained by calling LOOKUP-MODULE with the module name.  Modules whose name
-appears in BLACK-LIST are not loaded."
+appears in BLACK-LIST are not loaded. GET-OPTIONS is a procedure that takes a
+normalized module name string input and returns a string containing the complete
+parameters to pass to the module. See init_module(2) for the parameters format."
   (define (black-listed? module)
     (let ((result (member module black-list)))
       (when result
@@ -350,20 +353,23 @@ (define* (load-linux-module* file
     (let ((dependencies (module-dependencies file)))
       (every (cut load-linux-module* <>
                   #:lookup-module lookup-module
-                  #:black-list black-list)
+                  #:black-list black-list
+                  #:get-options get-options)
              (map lookup-module dependencies))))
 
   (and (not (black-listed? (file-name->module-name file)))
        (or (not recursive?)
            (load-dependencies file))
-       (let ((fd #f))
+       (let ((fd #f)
+             (options (get-options (file-name->module-name file))))
          (format (current-module-debugging-port)
-                 "loading Linux module from '~a'...~%" file)
+                 "loading Linux module from '~a' with options '~a'...~%" file
+                 options)
 
          (catch 'system-error
            (lambda ()
              (set! fd (open-fdes file O_RDONLY))
-             (load-linux-module/fd fd)
+             (load-linux-module/fd fd options)
              (close-fdes fd)
              #t)
            (lambda args

base-commit: f24b14767d362a84e6469682b4fe303b50f4b589
-- 
2.41.0

>From 903b2d5bcb1fa28121ffd46e70f6503559c0ec95 Mon Sep 17 00:00:00 2001
Message-ID: <903[email-masked]>
In-Reply-To: <626[email-masked]>
References: <626[email-masked]>
From: nathan <[email-masked]>
Date: Sat, 30 Dec 2023 22:11:35 -0500
Subject: [PATCH 2/3] linux-modules: Parse kernel command line and pass module
 arguments

* gnu/build/linux-modules.scm
(parse-kernel-cmdline): New procedure.
(load-linux-modules-from-directory): Use parse-kernel-cmdline to parse command
line and supply options to load-linux-module*.

Change-Id: I11a181670d0da5ea08965a80c396358e50bf5048
---
 gnu/build/linux-modules.scm | 98 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 97 insertions(+), 1 deletion(-)

diff --git a/gnu/build/linux-modules.scm b/gnu/build/linux-modules.scm
index c4950ea6d2..a54616b2d4 100644
--- a/gnu/build/linux-modules.scm
+++ b/gnu/build/linux-modules.scm
@@ -305,6 +305,93 @@ (define (modules-loaded)
       (((modules . _) ...)
        modules))))
 
+(define (parse-kernel-cmdline base)
+  "Input is the entire kernel command line, which is a space-delimited list of
+modulename.parameter=value parts.  The exact allowed syntax for the string is
+based on the kmod_config_parse_kcmdline function from modprobe.  Output is a
+vhash where the key is the module name as a string and the value is a list of
+strings in the form param=val (maybe with quotes in val).
+`normalize-module-name' is applied to the module names.
+
+Duplicate parameters are not removed.  The hyphen and underscore normalization
+in parameter names is not applied.  Empty module names are allowed."
+  (define (get-quoted start)
+    "It's similar to string-tokenize, but quoting is supported.  Two values
+are returned. First is the chunk of characters, second is the unused remainder
+of the string.  returns (values \"\" \"\") if the input string is just
+whitespace or there's an unterminated quote"
+    ;; strip whitespace at the start
+    (let ((start (substring
+                  start
+                  (or
+                   (string-skip start char-set:whitespace)
+                   (string-length start)))))
+      (let lp ((s start) (quoted? #f))
+        (if quoted?
+            (if (string-null? s)
+                (values "" "")
+                (lp (substring s 1) (not (eqv? (string-ref s 0) #\"))))
+            (if (or (string-null? s) (char-whitespace? (string-ref s 0)))
+                (values
+                 (substring start 0 (- (string-length start) (string-length s)))
+                 s)
+                (lp (substring s 1) (eqv? (string-ref s 0) #\")))))))
+  (let lp ((s base) (table vlist-null))
+    (call-with-values (lambda () (get-quoted s))
+      (lambda (strblock rest)
+        (if (string-null? strblock)
+            table
+            ;; get parameter name. it is mandatory.
+            (let ((dotpos (string-index strblock #\.)))
+              (if dotpos
+                  (let* ((equalpos (string-index strblock #\= dotpos))
+                         ;; modprobe allows a string like:
+                         ;; "modulename.param=val"
+                         ;; and then it converts it to
+                         ;; modulename.param="val"
+                         ;; This is the only case where a quote is
+                         ;; allowed outside of the value part.
+                         (is-bad-quoting (eqv? (string-ref strblock 0) #\"))
+                         (modulename
+                          (substring strblock (if is-bad-quoting 1 0) dotpos))
+                         (parampart
+                          (substring
+                           strblock
+                           (1+ dotpos)
+                           (or equalpos (string-length strblock)))))
+                    ;; whitespace is only allowed in value
+                    ;; no quotes allows in param segment
+                    ;; param cant have dot
+                    ;; module cant have equals
+                    (if (or
+                         (string-any (char-set-union
+                                      char-set:whitespace
+                                      (char-set #\" #\.))
+                                     parampart)
+                         (string-any (char-set-union
+                                      char-set:whitespace
+                                      (char-set #\=))
+                                     modulename))
+                        (lp rest table) ;; ignore and recur
+                        ;; add it to the table
+                        (let ((modulename (normalize-module-name modulename)))
+                          (lp rest
+                              (let ((oldval (vhash-assoc modulename table)))
+                                (vhash-cons
+                                 modulename
+                                 (cons
+                                  (if is-bad-quoting
+                                      (string-append
+                                       (substring strblock
+                                                  (1+ dotpos)
+                                                  (1+ equalpos))
+                                       "\"" (substring strblock (1+ equalpos)))
+                                      (substring strblock (1+ dotpos)))
+                                  (if oldval (cdr oldval) '()))
+                                 (vhash-delete modulename table)))))))
+                  (lp rest table))) ;; ignore and recur
+            )))))
+
 (define (module-black-list)
   "Return the black list of modules that must not be loaded.  This black list
 is specified using 'modprobe.blacklist=MODULE1,MODULE2,...' on the kernel
@@ -387,10 +474,19 @@ (define (load-linux-modules-from-directory modules directory)
 needed."
   (define module-name->file-name
     (module-name-lookup directory))
+  (define options-list
+    (parse-kernel-cmdline
+     (call-with-input-file "/proc/cmdline" get-string-all)))
+  (define (get-options modulename)
+    (let ((pair (vhash-assoc modulename options-list)))
+      (if pair
+          (string-join (cdr pair) " ")
+          "")))
 
   (for-each (lambda (module)
               (load-linux-module* (module-name->file-name module)
-                                  #:lookup-module module-name->file-name))
+                                  #:lookup-module module-name->file-name
+                                  #:get-options get-options))
             modules))
 
 
-- 
2.41.0

>From 0cd4f6fb19c96a6be4a0d0851e1cf6dfd7046a27 Mon Sep 17 00:00:00 2001
Message-ID: <0cd[email-masked]>
In-Reply-To: <626[email-masked]>
References: <626[email-masked]>
From: nathan <[email-masked]>
Date: Sat, 30 Dec 2023 22:12:19 -0500
Subject: [PATCH 3/3] linux-modules: Rewrite module-black-list with
 parse-kernel-cmdline

* gnu/build/linux-modules.scm
(module-black-list): Use parse-kernel-cmdline. Strip quotes from module names.
Normalize module names so they can be properly compared with strings from
file-name->module-name.

Change-Id: I318006f98844593863246fc89ed1703767794702
---
 gnu/build/linux-modules.scm | 31 +++++++++++++++++++------------
 1 file changed, 19 insertions(+), 12 deletions(-)

diff --git a/gnu/build/linux-modules.scm b/gnu/build/linux-modules.scm
index a54616b2d4..cbce3394e2 100644
--- a/gnu/build/linux-modules.scm
+++ b/gnu/build/linux-modules.scm
@@ -397,18 +397,25 @@ (define (module-black-list)
 is specified using 'modprobe.blacklist=MODULE1,MODULE2,...' on the kernel
 command line; it is honored by libkmod for users that pass
 'KMOD_PROBE_APPLY_BLACKLIST', which includes 'modprobe --use-blacklist' and
-udev."
-  (define parameter
-    "modprobe.blacklist=")
-
-  (let ((command (call-with-input-file "/proc/cmdline"
-                   get-string-all)))
-    (append-map (lambda (arg)
-                  (if (string-prefix? parameter arg)
-                      (string-tokenize (string-drop arg (string-length parameter))
-                                       %not-comma)
-                      '()))
-                (string-tokenize command))))
+udev.  The names in the returned list are normalized with `normalize-module-name'."
+  (define target-parameter "blacklist=")
+
+  (let* ((cmdline (parse-kernel-cmdline (call-with-input-file "/proc/cmdline"
+                                          get-string-all)))
+         (modprobe-pair (vhash-assoc "modprobe" cmdline)))
+    (if modprobe-pair
+        (map normalize-module-name
+         (append-map
+          (lambda (param)
+            (if (string-prefix? target-parameter param)
+                (string-tokenize
+                 (string-delete
+                  #\"
+                  (string-drop param (string-length target-parameter)))
+                 %not-comma)
+                '()))
+          (cdr modprobe-pair)))
+        '())))
 
 (define (module-loaded? module)
   "Return #t if MODULE is already loaded.  MODULE must be a Linux module name,
-- 
2.41.0