Skip to content

Commit 6cede27

Browse files
committed
kernel_cmdline: More improvements and fixes
- Removed `From<bytes::Parameter>` implementation for `utf8::Parameter` and similar for `utf8::ParameterKey`. This was public and would allow end-users to construct utf8 parameters from non-utf8 data. Replaced internally with `from_bytes` in the places where we know we can safely convert known-UTF-8 data. - Added `TryFrom<bytes::Paramter>` implementation for `utf8::Parameter` to allow checked conversions, plus tests. - Added `iter_utf8` and `find_utf8` to `bytes::Cmdline`, plus tests. - Updated `find_root_args_to_inherit` in bootc to use these improvements. Notably bootc will now allow non-UTF8 data in the kernel cmdline, *unless* it occurs in parameters that bootc is explicitly looking for. - Added more tests to `find_root_args_to_inherit` to validate expected functionality with non-UTF-8 data. - Fixed a parser bug that gemini pointed out with unmatched quotes, plus tests to check for that. Signed-off-by: John Eckersberg <[email protected]>
1 parent 5454cec commit 6cede27

File tree

4 files changed

+193
-38
lines changed

4 files changed

+193
-38
lines changed

crates/kernel_cmdline/src/bytes.rs

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
66
use std::borrow::Cow;
77

8+
use crate::utf8;
9+
810
use anyhow::Result;
911

1012
/// A parsed kernel command line.
@@ -64,6 +66,13 @@ impl<'a> Cmdline<'a> {
6466
CmdlineIter(&self.0)
6567
}
6668

69+
/// Returns an iterator over all parameters in the command line
70+
/// which are valid UTF-8.
71+
pub fn iter_utf8(&'a self) -> impl Iterator<Item = utf8::Parameter<'a>> {
72+
self.iter()
73+
.filter_map(|p| utf8::Parameter::try_from(p).ok())
74+
}
75+
6776
/// Locate a kernel argument with the given key name.
6877
///
6978
/// Returns the first parameter matching the given key, or `None` if not found.
@@ -73,14 +82,32 @@ impl<'a> Cmdline<'a> {
7382
self.iter().find(|p| p.key == key)
7483
}
7584

85+
/// Locate a kernel argument with the given key name.
86+
///
87+
/// Returns an error if a parameter with the given key name is
88+
/// found, but the value is not valid UTF-8.
89+
///
90+
/// Otherwise, returns the first parameter matching the given key,
91+
/// or `None` if not found. Key comparison treats dashes and
92+
/// underscores as equivalent.
93+
pub fn find_utf8(&'a self, key: impl AsRef<str>) -> Result<Option<utf8::Parameter<'a>>> {
94+
let bytes = match self.find(key.as_ref()) {
95+
Some(p) => p,
96+
None => return Ok(None),
97+
};
98+
99+
Ok(Some(utf8::Parameter::try_from(bytes)?))
100+
}
101+
76102
/// Find all kernel arguments starting with the given prefix.
77103
///
78104
/// This is a variant of [`Self::find`].
79-
pub fn find_all_starting_with(
105+
pub fn find_all_starting_with<T: AsRef<[u8]> + ?Sized>(
80106
&'a self,
81-
prefix: &'a [u8],
107+
prefix: &'a T,
82108
) -> impl Iterator<Item = Parameter<'a>> + 'a {
83-
self.iter().filter(move |p| p.key.0.starts_with(prefix))
109+
self.iter()
110+
.filter(move |p| p.key.0.starts_with(prefix.as_ref()))
84111
}
85112

86113
/// Locate the value of the kernel argument with the given key name.
@@ -199,11 +226,10 @@ impl<'a> Parameter<'a> {
199226
value = &value[1..];
200227

201228
// *Only* the first and last double quotes are stripped
202-
value = value
203-
.strip_prefix(b"\"")
204-
.unwrap_or(value)
205-
.strip_suffix(b"\"")
206-
.unwrap_or(value);
229+
value = {
230+
let v = value.strip_prefix(b"\"").unwrap_or(value);
231+
v.strip_suffix(b"\"").unwrap_or(v)
232+
};
207233

208234
Self {
209235
key,
@@ -237,11 +263,15 @@ impl<'a> PartialEq for Parameter<'a> {
237263
mod tests {
238264
use super::*;
239265

240-
// convenience method for tests
266+
// convenience methods for tests
241267
fn param(s: &str) -> Parameter<'_> {
242268
Parameter::parse(s.as_bytes()).0.unwrap()
243269
}
244270

271+
fn param_utf8(s: &str) -> utf8::Parameter<'_> {
272+
utf8::Parameter::parse(s).0.unwrap()
273+
}
274+
245275
#[test]
246276
fn test_parameter_parse() {
247277
let (p, rest) = Parameter::parse(b"foo");
@@ -281,6 +311,12 @@ mod tests {
281311
fn test_parameter_quoted() {
282312
let p = param("foo=\"quoted value\"");
283313
assert_eq!(p.value, Some(b"quoted value".as_slice()));
314+
315+
let p = param("foo=\"unclosed quotes");
316+
assert_eq!(p.value, Some(b"unclosed quotes".as_slice()));
317+
318+
let p = param("foo=trailing_quotes\"");
319+
assert_eq!(p.value, Some(b"trailing_quotes".as_slice()));
284320
}
285321

286322
#[test]
@@ -369,6 +405,38 @@ mod tests {
369405
assert!(kargs.find("nothing").is_none());
370406
}
371407

408+
#[test]
409+
fn test_kargs_iter_utf8() {
410+
let kargs = Cmdline::from(b"foo=bar,bar2 \xff baz=fuz bad=oh\xffno wiz");
411+
let mut iter = kargs.iter_utf8();
412+
413+
assert_eq!(iter.next(), Some(param_utf8("foo=bar,bar2")));
414+
assert_eq!(iter.next(), Some(param_utf8("baz=fuz")));
415+
assert_eq!(iter.next(), Some(param_utf8("wiz")));
416+
assert_eq!(iter.next(), None);
417+
}
418+
419+
#[test]
420+
fn test_kargs_find_utf8() {
421+
let kargs = Cmdline::from(b"foo=bar,bar2 \xff baz=fuz bad=oh\xffno wiz");
422+
423+
// found it
424+
assert_eq!(
425+
kargs.find_utf8("foo").unwrap().unwrap().value().unwrap(),
426+
"bar,bar2"
427+
);
428+
429+
// didn't find it
430+
assert!(kargs.find_utf8("nothing").unwrap().is_none());
431+
432+
// found it but key is invalid
433+
let p = kargs.find_utf8("bad");
434+
assert_eq!(
435+
p.unwrap_err().to_string(),
436+
"Parameter value is not valid UTF-8"
437+
);
438+
}
439+
372440
#[test]
373441
fn test_kargs_from_proc() {
374442
let kargs = Cmdline::from_proc().unwrap();

crates/kernel_cmdline/src/lib.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,3 @@
1212
1313
pub mod bytes;
1414
pub mod utf8;
15-
16-
/// This is used by dracut.
17-
pub const INITRD_ARG_PREFIX: &str = "rd.";
18-
/// The kernel argument for configuring the rootfs flags.
19-
pub const ROOTFLAGS: &str = "rootflags";

crates/kernel_cmdline/src/utf8.rs

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
//! This module provides functionality for parsing and working with kernel command line
44
//! arguments, supporting both key-only switches and key-value pairs with proper quote handling.
55
6+
use std::ops::Deref;
7+
68
use crate::bytes;
79

810
use anyhow::Result;
@@ -34,7 +36,7 @@ impl<'a> Iterator for CmdlineIter<'a> {
3436
type Item = Parameter<'a>;
3537

3638
fn next(&mut self) -> Option<Self::Item> {
37-
self.0.next().map(Into::into)
39+
self.0.next().map(Parameter::from_bytes)
3840
}
3941
}
4042

@@ -71,7 +73,8 @@ impl<'a> Cmdline<'a> {
7173
/// Returns the first parameter matching the given key, or `None` if not found.
7274
/// Key comparison treats dashes and underscores as equivalent.
7375
pub fn find(&'a self, key: impl AsRef<str>) -> Option<Parameter<'a>> {
74-
self.iter().find(|p| p.key() == key.as_ref().into())
76+
let key = ParameterKey::from(key.as_ref());
77+
self.iter().find(|p| p.key() == key)
7578
}
7679

7780
/// Find all kernel arguments starting with the given UTF-8 prefix.
@@ -122,15 +125,19 @@ impl<'a> std::ops::Deref for ParameterKey<'a> {
122125
}
123126
}
124127

125-
impl<'a> From<&'a str> for ParameterKey<'a> {
126-
fn from(input: &'a str) -> Self {
127-
Self(bytes::ParameterKey(input.as_bytes()))
128+
impl<'a> ParameterKey<'a> {
129+
/// Construct a utf8::ParameterKey from a bytes::ParameterKey
130+
///
131+
/// This is non-public and should only be used when the underlying
132+
/// bytes are known to be valid UTF-8.
133+
fn from_bytes(input: bytes::ParameterKey<'a>) -> Self {
134+
Self(input)
128135
}
129136
}
130137

131-
impl<'a> From<bytes::ParameterKey<'a>> for ParameterKey<'a> {
132-
fn from(input: bytes::ParameterKey<'a>) -> Self {
133-
Self(input)
138+
impl<'a> From<&'a str> for ParameterKey<'a> {
139+
fn from(input: &'a str) -> Self {
140+
Self(bytes::ParameterKey(input.as_bytes()))
134141
}
135142
}
136143

@@ -181,9 +188,17 @@ impl<'a> Parameter<'a> {
181188
}
182189
}
183190

191+
/// Construct a utf8::Parameter from a bytes::Parameter
192+
///
193+
/// This is non-public and should only be used when the underlying
194+
/// bytes are known to be valid UTF-8.
195+
fn from_bytes(bytes: bytes::Parameter<'a>) -> Self {
196+
Self(bytes)
197+
}
198+
184199
/// Returns the key part of the parameter
185200
pub fn key(&'a self) -> ParameterKey<'a> {
186-
self.0.key().into()
201+
ParameterKey::from_bytes(self.0.key())
187202
}
188203

189204
/// Returns the optional value part of the parameter
@@ -196,9 +211,21 @@ impl<'a> Parameter<'a> {
196211
}
197212
}
198213

199-
impl<'a> From<bytes::Parameter<'a>> for Parameter<'a> {
200-
fn from(bytes: bytes::Parameter<'a>) -> Self {
201-
Self(bytes)
214+
impl<'a> TryFrom<bytes::Parameter<'a>> for Parameter<'a> {
215+
type Error = anyhow::Error;
216+
217+
fn try_from(bytes: bytes::Parameter<'a>) -> Result<Self, Self::Error> {
218+
if str::from_utf8(bytes.key().deref()).is_err() {
219+
anyhow::bail!("Parameter key is not valid UTF-8");
220+
}
221+
222+
if let Some(value) = bytes.value() {
223+
if str::from_utf8(value).is_err() {
224+
anyhow::bail!("Parameter value is not valid UTF-8");
225+
}
226+
}
227+
228+
Ok(Self(bytes))
202229
}
203230
}
204231

@@ -331,6 +358,37 @@ mod tests {
331358
assert_ne!(switch, keyvalue);
332359
}
333360

361+
#[test]
362+
fn test_parameter_tryfrom() {
363+
// ok switch
364+
let p = bytes::Parameter::parse(b"foo").0.unwrap();
365+
let utf = Parameter::try_from(p).unwrap();
366+
assert_eq!(utf.key(), "foo".into());
367+
assert_eq!(utf.value(), None);
368+
369+
// ok key/value
370+
let p = bytes::Parameter::parse(b"foo=bar").0.unwrap();
371+
let utf = Parameter::try_from(p).unwrap();
372+
assert_eq!(utf.key(), "foo".into());
373+
assert_eq!(utf.value(), Some("bar".into()));
374+
375+
// bad switch
376+
let p = bytes::Parameter::parse(b"f\xffoo").0.unwrap();
377+
let e = Parameter::try_from(p);
378+
assert_eq!(
379+
e.unwrap_err().to_string(),
380+
"Parameter key is not valid UTF-8"
381+
);
382+
383+
// bad key/value
384+
let p = bytes::Parameter::parse(b"foo=b\xffar").0.unwrap();
385+
let e = Parameter::try_from(p);
386+
assert_eq!(
387+
e.unwrap_err().to_string(),
388+
"Parameter value is not valid UTF-8"
389+
);
390+
}
391+
334392
#[test]
335393
fn test_kargs_simple() {
336394
// example taken lovingly from:

crates/lib/src/install.rs

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ use crate::spec::ImageReference;
6262
use crate::store::Storage;
6363
use crate::task::Task;
6464
use crate::utils::sigpolicy_from_opt;
65-
use bootc_kernel_cmdline::utf8::Cmdline;
65+
use bootc_kernel_cmdline::{bytes, utf8};
6666
use bootc_mount::Filesystem;
6767

6868
/// The toplevel boot directory
@@ -83,6 +83,10 @@ const SELINUXFS: &str = "/sys/fs/selinux";
8383
/// The mount path for uefi
8484
const EFIVARFS: &str = "/sys/firmware/efi/efivars";
8585
pub(crate) const ARCH_USES_EFI: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64"));
86+
/// This is used by dracut.
87+
pub const INITRD_ARG_PREFIX: &str = "rd.";
88+
/// The kernel argument for configuring the rootfs flags.
89+
pub const ROOTFLAGS: &str = "rootflags";
8690

8791
const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[
8892
// Default to avoiding grub2-mkconfig etc.
@@ -1653,18 +1657,24 @@ struct RootMountInfo {
16531657

16541658
/// Discover how to mount the root filesystem, using existing kernel arguments and information
16551659
/// about the root mount.
1656-
fn find_root_args_to_inherit(cmdline: &Cmdline, root_info: &Filesystem) -> Result<RootMountInfo> {
1660+
fn find_root_args_to_inherit(
1661+
cmdline: &bytes::Cmdline,
1662+
root_info: &Filesystem,
1663+
) -> Result<RootMountInfo> {
16571664
// If we have a root= karg, then use that
1658-
let (mount_spec, kargs) = if let Some(root) = cmdline.value_of("root") {
1659-
let rootflags = cmdline.find(bootc_kernel_cmdline::ROOTFLAGS);
1660-
let inherit_kargs = cmdline.find_all_starting_with(bootc_kernel_cmdline::INITRD_ARG_PREFIX);
1665+
let root = cmdline
1666+
.find_utf8("root")?
1667+
.and_then(|p| p.value().map(|p| p.to_string()));
1668+
let (mount_spec, kargs) = if let Some(root) = root {
1669+
let rootflags = cmdline.find(ROOTFLAGS);
1670+
let inherit_kargs = cmdline.find_all_starting_with(INITRD_ARG_PREFIX);
16611671
(
1662-
root.to_owned(),
1672+
root,
16631673
rootflags
16641674
.into_iter()
16651675
.chain(inherit_kargs)
1666-
.map(|p| p.to_string())
1667-
.collect(),
1676+
.map(|p| utf8::Parameter::try_from(p).map(|p| p.to_string()))
1677+
.collect::<Result<Vec<_>, _>>()?,
16681678
)
16691679
} else {
16701680
let uuid = root_info
@@ -1832,7 +1842,7 @@ pub(crate) async fn install_to_filesystem(
18321842
}
18331843
} else if targeting_host_root {
18341844
// In the to-existing-root case, look at /proc/cmdline
1835-
let cmdline = Cmdline::from_proc()?;
1845+
let cmdline = bytes::Cmdline::from_proc()?;
18361846
find_root_args_to_inherit(&cmdline, &inspect)?
18371847
} else {
18381848
// Otherwise, gather metadata from the provided root and use its provided UUID as a
@@ -2085,18 +2095,42 @@ mod tests {
20852095
uuid: Some("965eb3c7-5a3f-470d-aaa2-1bcf04334bc6".into()),
20862096
children: None,
20872097
};
2088-
let kargs = Cmdline::from("");
2098+
let kargs = bytes::Cmdline::from("");
20892099
let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
20902100
assert_eq!(r.mount_spec, "UUID=965eb3c7-5a3f-470d-aaa2-1bcf04334bc6");
20912101

2092-
let kargs =
2093-
Cmdline::from("root=/dev/mapper/root rw someother=karg rd.lvm.lv=root systemd.debug=1");
2102+
let kargs = bytes::Cmdline::from(
2103+
"root=/dev/mapper/root rw someother=karg rd.lvm.lv=root systemd.debug=1",
2104+
);
20942105

20952106
// In this case we take the root= from the kernel cmdline
20962107
let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
20972108
assert_eq!(r.mount_spec, "/dev/mapper/root");
20982109
assert_eq!(r.kargs.len(), 1);
20992110
assert_eq!(r.kargs[0], "rd.lvm.lv=root");
2111+
2112+
// non-UTF8 data in non-essential parts of the cmdline should be ignored
2113+
let kargs = bytes::Cmdline::from(
2114+
b"root=/dev/mapper/root rw non-utf8=\xff rd.lvm.lv=root systemd.debug=1",
2115+
);
2116+
let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2117+
assert_eq!(r.mount_spec, "/dev/mapper/root");
2118+
assert_eq!(r.kargs.len(), 1);
2119+
assert_eq!(r.kargs[0], "rd.lvm.lv=root");
2120+
2121+
// non-UTF8 data in `root` should fail
2122+
let kargs = bytes::Cmdline::from(
2123+
b"root=/dev/mapper/ro\xffot rw non-utf8=\xff rd.lvm.lv=root systemd.debug=1",
2124+
);
2125+
let r = find_root_args_to_inherit(&kargs, &inspect);
2126+
assert!(r.is_err());
2127+
2128+
// non-UTF8 data in `rd.` should fail
2129+
let kargs = bytes::Cmdline::from(
2130+
b"root=/dev/mapper/root rw non-utf8=\xff rd.lvm.lv=ro\xffot systemd.debug=1",
2131+
);
2132+
let r = find_root_args_to_inherit(&kargs, &inspect);
2133+
assert!(r.is_err());
21002134
}
21012135

21022136
// As this is a unit test we don't try to test mountpoints, just verify

0 commit comments

Comments
 (0)