Skip to content

Commit 1532e74

Browse files
authored
Merge pull request #1476 from jeckersb/kernel-cmdline-v2
Improve kernel argument parsing
2 parents 7578551 + cf7f150 commit 1532e74

File tree

2 files changed

+254
-58
lines changed

2 files changed

+254
-58
lines changed

crates/lib/src/install.rs

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ use self::baseline::InstallBlockDeviceOpts;
5555
use crate::boundimage::{BoundImage, ResolvedBoundImage};
5656
use crate::containerenv::ContainerExecutionInfo;
5757
use crate::deploy::{prepare_for_pull, pull_from_prepared, PreparedImportMeta, PreparedPullResult};
58+
use crate::kernel::Cmdline;
5859
use crate::lsm;
5960
use crate::progress_jsonl::ProgressWriter;
6061
use crate::spec::ImageReference;
@@ -1661,20 +1662,22 @@ struct RootMountInfo {
16611662

16621663
/// Discover how to mount the root filesystem, using existing kernel arguments and information
16631664
/// about the root mount.
1664-
fn find_root_args_to_inherit(cmdline: &[&str], root_info: &Filesystem) -> Result<RootMountInfo> {
1665-
let cmdline = || cmdline.iter().copied();
1666-
let root = crate::kernel::find_first_cmdline_arg(cmdline(), "root");
1665+
fn find_root_args_to_inherit(cmdline: &Cmdline, root_info: &Filesystem) -> Result<RootMountInfo> {
1666+
let root = cmdline.iter().find(|p| p.key == b"root");
16671667
// If we have a root= karg, then use that
16681668
let (mount_spec, kargs) = if let Some(root) = root {
1669-
let rootflags = cmdline().find(|arg| arg.starts_with(crate::kernel::ROOTFLAGS));
1670-
let inherit_kargs =
1671-
cmdline().filter(|arg| arg.starts_with(crate::kernel::INITRD_ARG_PREFIX));
1669+
let rootflags = cmdline
1670+
.iter()
1671+
.find(|arg| arg.key == crate::kernel::ROOTFLAGS);
1672+
let inherit_kargs = cmdline
1673+
.iter()
1674+
.filter(|arg| arg.key.starts_with(crate::kernel::INITRD_ARG_PREFIX));
16721675
(
1673-
root.to_owned(),
1676+
root.value_lossy(),
16741677
rootflags
16751678
.into_iter()
16761679
.chain(inherit_kargs)
1677-
.map(ToOwned::to_owned)
1680+
.map(|p| p.to_string())
16781681
.collect(),
16791682
)
16801683
} else {
@@ -1823,8 +1826,7 @@ pub(crate) async fn install_to_filesystem(
18231826
}
18241827
} else if targeting_host_root {
18251828
// In the to-existing-root case, look at /proc/cmdline
1826-
let cmdline = crate::kernel::parse_cmdline()?;
1827-
let cmdline = cmdline.iter().map(|s| s.as_str()).collect::<Vec<_>>();
1829+
let cmdline = Cmdline::from_proc()?;
18281830
find_root_args_to_inherit(&cmdline, &inspect)?
18291831
} else {
18301832
// Otherwise, gather metadata from the provided root and use its provided UUID as a
@@ -2035,21 +2037,15 @@ mod tests {
20352037
uuid: Some("965eb3c7-5a3f-470d-aaa2-1bcf04334bc6".into()),
20362038
children: None,
20372039
};
2038-
let r = find_root_args_to_inherit(&[], &inspect).unwrap();
2040+
let kargs = Cmdline::from("");
2041+
let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
20392042
assert_eq!(r.mount_spec, "UUID=965eb3c7-5a3f-470d-aaa2-1bcf04334bc6");
20402043

2044+
let kargs =
2045+
Cmdline::from("root=/dev/mapper/root rw someother=karg rd.lvm.lv=root systemd.debug=1");
2046+
20412047
// In this case we take the root= from the kernel cmdline
2042-
let r = find_root_args_to_inherit(
2043-
&[
2044-
"root=/dev/mapper/root",
2045-
"rw",
2046-
"someother=karg",
2047-
"rd.lvm.lv=root",
2048-
"systemd.debug=1",
2049-
],
2050-
&inspect,
2051-
)
2052-
.unwrap();
2048+
let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
20532049
assert_eq!(r.mount_spec, "/dev/mapper/root");
20542050
assert_eq!(r.kargs.len(), 1);
20552051
assert_eq!(r.kargs[0], "rd.lvm.lv=root");

crates/lib/src/kernel.rs

Lines changed: 236 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,251 @@
1+
use std::borrow::Cow;
2+
13
use anyhow::Result;
2-
use fn_error_context::context;
34

45
/// This is used by dracut.
5-
pub(crate) const INITRD_ARG_PREFIX: &str = "rd.";
6+
pub(crate) const INITRD_ARG_PREFIX: &[u8] = b"rd.";
67
/// The kernel argument for configuring the rootfs flags.
7-
pub(crate) const ROOTFLAGS: &str = "rootflags=";
8-
9-
/// Parse the kernel command line. This is strictly
10-
/// speaking not a correct parser, as the Linux kernel
11-
/// supports quotes. However, we don't yet need that here.
12-
///
13-
/// See systemd's code for one userspace parser.
14-
#[context("Reading /proc/cmdline")]
15-
pub(crate) fn parse_cmdline() -> Result<Vec<String>> {
16-
let cmdline = std::fs::read_to_string("/proc/cmdline")?;
17-
let r = cmdline
18-
.split_ascii_whitespace()
19-
.map(ToOwned::to_owned)
20-
.collect();
21-
Ok(r)
22-
}
23-
24-
/// Return the value for the string in the vector which has the form target_key=value
25-
pub(crate) fn find_first_cmdline_arg<'a>(
26-
args: impl Iterator<Item = &'a str>,
27-
target_key: &str,
28-
) -> Option<&'a str> {
29-
args.filter_map(|arg| {
30-
if let Some((k, v)) = arg.split_once('=') {
31-
if target_key == k {
32-
return Some(v);
8+
pub(crate) const ROOTFLAGS: &[u8] = b"rootflags";
9+
10+
pub(crate) struct Cmdline<'a>(Cow<'a, [u8]>);
11+
12+
impl<'a, T: AsRef<[u8]> + ?Sized> From<&'a T> for Cmdline<'a> {
13+
fn from(input: &'a T) -> Self {
14+
Self(Cow::Borrowed(input.as_ref()))
15+
}
16+
}
17+
18+
impl<'a> Cmdline<'a> {
19+
pub fn from_proc() -> Result<Self> {
20+
Ok(Self(Cow::Owned(std::fs::read("/proc/cmdline")?)))
21+
}
22+
23+
pub fn iter(&'a self) -> impl Iterator<Item = Parameter<'a>> {
24+
let mut in_quotes = false;
25+
26+
self.0
27+
.split(move |c| {
28+
if *c == b'"' {
29+
in_quotes = !in_quotes;
30+
}
31+
!in_quotes && c.is_ascii_whitespace()
32+
})
33+
.map(Parameter::from)
34+
}
35+
}
36+
37+
#[derive(Debug, Eq)]
38+
pub(crate) struct Parameter<'a> {
39+
pub key: &'a [u8],
40+
pub value: Option<&'a [u8]>,
41+
}
42+
43+
impl<'a> Parameter<'a> {
44+
pub fn key_lossy(&self) -> String {
45+
String::from_utf8_lossy(self.key).to_string()
46+
}
47+
48+
pub fn value_lossy(&self) -> String {
49+
String::from_utf8_lossy(self.value.unwrap_or(&[])).to_string()
50+
}
51+
}
52+
53+
impl<'a, T: AsRef<[u8]> + ?Sized> From<&'a T> for Parameter<'a> {
54+
fn from(input: &'a T) -> Self {
55+
let input = input.as_ref();
56+
let equals = input.iter().position(|b| *b == b'=');
57+
58+
match equals {
59+
None => Self {
60+
key: input,
61+
value: None,
62+
},
63+
Some(i) => {
64+
let (key, mut value) = input.split_at(i);
65+
66+
// skip `=`, we know it's the first byte because we
67+
// found it above
68+
value = &value[1..];
69+
70+
// *Only* the first and last double quotes are stripped
71+
value = value
72+
.strip_prefix(b"\"")
73+
.unwrap_or(value)
74+
.strip_suffix(b"\"")
75+
.unwrap_or(value);
76+
77+
Self {
78+
key,
79+
value: Some(value),
80+
}
81+
}
82+
}
83+
}
84+
}
85+
86+
impl PartialEq for Parameter<'_> {
87+
fn eq(&self, other: &Self) -> bool {
88+
let dedashed = |&c: &u8| {
89+
if c == b'-' {
90+
b'_'
91+
} else {
92+
c
93+
}
94+
};
95+
96+
// We can't just zip() because leading substrings will match
97+
//
98+
// For example, "foo" == "foobar" since the zipped iterator
99+
// only compares the first three chars.
100+
let our_iter = self.key.iter().map(dedashed);
101+
let other_iter = other.key.iter().map(dedashed);
102+
if !our_iter.eq(other_iter) {
103+
return false;
104+
}
105+
106+
match (self.value, other.value) {
107+
(Some(ours), Some(other)) => ours == other,
108+
(None, None) => true,
109+
_ => false,
110+
}
111+
}
112+
}
113+
114+
impl std::fmt::Display for Parameter<'_> {
115+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
116+
let key = self.key_lossy();
117+
118+
if self.value.is_some() {
119+
let value = self.value_lossy();
120+
121+
if value.chars().any(|c| c.is_ascii_whitespace()) {
122+
write!(f, "{key}=\"{value}\"")
123+
} else {
124+
write!(f, "{key}={value}")
33125
}
126+
} else {
127+
write!(f, "{key}")
34128
}
35-
None
36-
})
37-
.next()
129+
}
38130
}
39131

40132
#[cfg(test)]
41133
mod tests {
42134
use super::*;
43135

44136
#[test]
45-
fn test_find_first() {
46-
let kargs = &["foo=bar", "root=/dev/vda", "blah", "root=/dev/other"];
47-
let kargs = || kargs.iter().copied();
48-
assert_eq!(find_first_cmdline_arg(kargs(), "root"), Some("/dev/vda"));
49-
assert_eq!(find_first_cmdline_arg(kargs(), "nonexistent"), None);
137+
fn test_parameter_simple() {
138+
let switch = Parameter::from("foo");
139+
assert_eq!(switch.key, b"foo");
140+
assert_eq!(switch.value, None);
141+
142+
let kv = Parameter::from("bar=baz");
143+
assert_eq!(kv.key, b"bar");
144+
assert_eq!(kv.value, Some(b"baz".as_slice()));
145+
}
146+
147+
#[test]
148+
fn test_parameter_quoted() {
149+
let p = Parameter::from("foo=\"quoted value\"");
150+
assert_eq!(p.value, Some(b"quoted value".as_slice()));
151+
}
152+
153+
#[test]
154+
fn test_parameter_pathological() {
155+
// valid things that certified insane people would do
156+
157+
// quotes don't get removed from keys
158+
let p = Parameter::from("\"\"\"");
159+
assert_eq!(p.key, b"\"\"\"");
160+
161+
// quotes only get stripped from the absolute ends of values
162+
let p = Parameter::from("foo=\"internal \" quotes \" are ok\"");
163+
assert_eq!(p.value, Some(b"internal \" quotes \" are ok".as_slice()));
164+
165+
// non-UTF8 things are in fact valid
166+
let non_utf8_byte = b"\xff";
167+
#[allow(invalid_from_utf8)]
168+
let failed_conversion = str::from_utf8(non_utf8_byte);
169+
assert!(failed_conversion.is_err());
170+
let mut p = b"foo=".to_vec();
171+
p.push(non_utf8_byte[0]);
172+
let p = Parameter::from(&p);
173+
assert_eq!(p.value, Some(non_utf8_byte.as_slice()));
174+
175+
// lossy replacement sanity check
176+
assert_eq!(p.value_lossy(), char::REPLACEMENT_CHARACTER.to_string());
177+
}
178+
179+
#[test]
180+
fn test_parameter_equality() {
181+
// substrings are not equal
182+
let foo = Parameter::from("foo");
183+
let bar = Parameter::from("foobar");
184+
assert_ne!(foo, bar);
185+
assert_ne!(bar, foo);
186+
187+
// dashes and underscores are treated equally
188+
let dashes = Parameter::from("a-delimited-param");
189+
let underscores = Parameter::from("a_delimited_param");
190+
assert_eq!(dashes, underscores);
191+
192+
// same key, same values is equal
193+
let dashes = Parameter::from("a-delimited-param=same_values");
194+
let underscores = Parameter::from("a_delimited_param=same_values");
195+
assert_eq!(dashes, underscores);
196+
197+
// same key, different values is not equal
198+
let dashes = Parameter::from("a-delimited-param=different_values");
199+
let underscores = Parameter::from("a_delimited_param=DiFfErEnT_valUEZ");
200+
assert_ne!(dashes, underscores);
201+
202+
// mixed variants are never equal
203+
let switch = Parameter::from("same_key");
204+
let keyvalue = Parameter::from("same_key=but_with_a_value");
205+
assert_ne!(switch, keyvalue);
206+
}
207+
208+
#[test]
209+
fn test_kargs_simple() {
210+
// example taken lovingly from:
211+
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/kernel/params.c?id=89748acdf226fd1a8775ff6fa2703f8412b286c8#n160
212+
let kargs = Cmdline::from(b"foo=bar,bar2 baz=fuz wiz".as_slice());
213+
let mut iter = kargs.iter();
214+
215+
assert_eq!(
216+
iter.next(),
217+
Some(Parameter {
218+
key: b"foo",
219+
value: Some(b"bar,bar2".as_slice())
220+
})
221+
);
222+
223+
assert_eq!(
224+
iter.next(),
225+
Some(Parameter {
226+
key: b"baz",
227+
value: Some(b"fuz".as_slice())
228+
})
229+
);
230+
231+
assert_eq!(
232+
iter.next(),
233+
Some(Parameter {
234+
key: b"wiz",
235+
value: None,
236+
})
237+
);
238+
239+
assert_eq!(iter.next(), None);
240+
}
241+
242+
#[test]
243+
fn test_kargs_from_proc() {
244+
let kargs = Cmdline::from_proc().unwrap();
245+
246+
// Not really a good way to test this other than assume
247+
// there's at least one argument in /proc/cmdline wherever the
248+
// tests are running
249+
assert!(kargs.iter().count() > 0);
50250
}
51251
}

0 commit comments

Comments
 (0)