-
Notifications
You must be signed in to change notification settings - Fork 181
fix: drop unsafe host env vars when envp is NULL #661
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1303,6 +1303,45 @@ pub unsafe extern "C" fn krun_set_workdir(ctx_id: u32, c_workdir_path: *const c_ | |||||||||||||||||||
| KRUN_SUCCESS | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Soft cap on the size of the env section inherited from the host process when | ||||||||||||||||||||
| /// the caller passes `envp = NULL` to `krun_set_exec`/`krun_set_env`. Linux | ||||||||||||||||||||
| /// genreally accepts only the first 2048 bytes so setting this value to 1024 leaves | ||||||||||||||||||||
| /// room for default options (KRUN_INIT, KRUN_RLIMITS, etc.). | ||||||||||||||||||||
| const MAX_INHERITED_ENV_BYTES: usize = 1024; | ||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. about this constant, cmdline size limits vary for different archs, for example
Suggested change
Or is the 1024 limit intentional? nit: s/genreally/generally
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dorindabassey Keep in mind the Also TBH, I wouldn't worry too much about this, we can never do this fully correctly (as the comment this PR added mentions it's best effort). In the long term we should stop using this to pass the arguments. That said its possible to "optimize" this to pick a better value. I'm just not sure It's good use of time to worry about this, this argument passing mechanism is inherently broken. |
||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Best-effort inheritance of the host process's environment for the guest's | ||||||||||||||||||||
| /// kernel cmdline. Vars whose key or value contain anything outside printable | ||||||||||||||||||||
| /// ASCII (excluding space) are dropped, since the Linux kernel cmdline parser | ||||||||||||||||||||
| /// tokenizes at whitespace and `Cmdline::valid_str` rejects non-printable | ||||||||||||||||||||
| /// chars. Each var is also dropped individually if appending it would push | ||||||||||||||||||||
| /// the total over `MAX_INHERITED_ENV_BYTES`, so a single oversized var (e.g. | ||||||||||||||||||||
| /// LS_COLORS) doesn't take out smaller vars iterated after it. | ||||||||||||||||||||
| fn inherit_host_env() -> String { | ||||||||||||||||||||
| serialize_env(env::vars()) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+1319
to
+1321
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using fn inherit_host_env() -> String {
serialize_env(env::vars_os().filter_map(|(k, v)| {
match (k.into_string(), v.into_string()) {
(Ok(k), Ok(v)) => Some((k, v)),
_ => None,
}
}))
} |
||||||||||||||||||||
|
|
||||||||||||||||||||
| fn serialize_env<I: IntoIterator<Item = (String, String)>>(vars: I) -> String { | ||||||||||||||||||||
| let mut buf = String::new(); | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||
| for (k, v) in vars { | ||||||||||||||||||||
| if k.is_empty() | ||||||||||||||||||||
| || k.contains('=') | ||||||||||||||||||||
| || !k.chars().all(|c| c.is_ascii_graphic()) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| continue; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if !v.chars().all(|c| c.is_ascii_graphic()) { | ||||||||||||||||||||
| continue; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let entry = format!(" {k}={v}"); | ||||||||||||||||||||
| if buf.len() + entry.len() > MAX_INHERITED_ENV_BYTES { | ||||||||||||||||||||
| continue; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| buf.push_str(&entry); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| buf | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| unsafe fn collapse_str_array(array: &[*const c_char]) -> Result<String, std::str::Utf8Error> { | ||||||||||||||||||||
| let mut strvec = Vec::new(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -1318,7 +1357,6 @@ unsafe fn collapse_str_array(array: &[*const c_char]) -> Result<String, std::str | |||||||||||||||||||
| Ok(strvec.join(" ")) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[allow(clippy::format_collect)] | ||||||||||||||||||||
| #[allow(clippy::missing_safety_doc)] | ||||||||||||||||||||
| #[no_mangle] | ||||||||||||||||||||
| pub unsafe extern "C" fn krun_set_exec( | ||||||||||||||||||||
|
|
@@ -1358,9 +1396,7 @@ pub unsafe extern "C" fn krun_set_exec( | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| env::vars() | ||||||||||||||||||||
| .map(|(key, value)| format!(" {key}=\"{value}\"")) | ||||||||||||||||||||
| .collect() | ||||||||||||||||||||
| inherit_host_env() | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| match CTX_MAP.lock().unwrap().entry(ctx_id) { | ||||||||||||||||||||
|
|
@@ -1376,7 +1412,6 @@ pub unsafe extern "C" fn krun_set_exec( | |||||||||||||||||||
| KRUN_SUCCESS | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[allow(clippy::format_collect)] | ||||||||||||||||||||
| #[allow(clippy::missing_safety_doc)] | ||||||||||||||||||||
| #[no_mangle] | ||||||||||||||||||||
| pub unsafe extern "C" fn krun_set_env(ctx_id: u32, c_envp: *const *const c_char) -> i32 { | ||||||||||||||||||||
|
|
@@ -1390,9 +1425,7 @@ pub unsafe extern "C" fn krun_set_env(ctx_id: u32, c_envp: *const *const c_char) | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| env::vars() | ||||||||||||||||||||
| .map(|(key, value)| format!(" {key}=\"{value}\"")) | ||||||||||||||||||||
| .collect() | ||||||||||||||||||||
| inherit_host_env() | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| match CTX_MAP.lock().unwrap().entry(ctx_id) { | ||||||||||||||||||||
|
|
@@ -2786,3 +2819,92 @@ fn krun_start_enter_nitro(ctx_id: u32) -> i32 { | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[cfg(test)] | ||||||||||||||||||||
| mod tests { | ||||||||||||||||||||
| use super::*; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| fn vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> { | ||||||||||||||||||||
| pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect() | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[test] | ||||||||||||||||||||
| fn serialize_env_passes_clean_vars() { | ||||||||||||||||||||
| let out = serialize_env(vars(&[("HOME", "/root"), ("PATH", "/usr/bin")])); | ||||||||||||||||||||
| assert_eq!(out, " HOME=/root PATH=/usr/bin"); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[test] | ||||||||||||||||||||
| fn serialize_env_drops_value_with_space() { | ||||||||||||||||||||
| let out = serialize_env(vars(&[("OLDPWD", "/some path"), ("HOME", "/root")])); | ||||||||||||||||||||
| assert_eq!(out, " HOME=/root"); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[test] | ||||||||||||||||||||
| fn serialize_env_drops_key_with_space() { | ||||||||||||||||||||
| let out = serialize_env(vars(&[("BAD KEY", "v"), ("HOME", "/root")])); | ||||||||||||||||||||
| assert_eq!(out, " HOME=/root"); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[test] | ||||||||||||||||||||
| fn serialize_env_drops_key_containing_equals() { | ||||||||||||||||||||
| let out = serialize_env(vars(&[("K=Y", "v"), ("HOME", "/root")])); | ||||||||||||||||||||
| assert_eq!(out, " HOME=/root"); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[test] | ||||||||||||||||||||
| fn serialize_env_drops_non_ascii() { | ||||||||||||||||||||
| let out = serialize_env(vars(&[("LANG", "en_US.üTF8"), ("HOME", "/root")])); | ||||||||||||||||||||
| assert_eq!(out, " HOME=/root"); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[test] | ||||||||||||||||||||
| fn serialize_env_drops_control_chars() { | ||||||||||||||||||||
| let out = serialize_env(vars(&[("X", "a\tb"), ("HOME", "/root")])); | ||||||||||||||||||||
| assert_eq!(out, " HOME=/root"); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[test] | ||||||||||||||||||||
| fn serialize_env_truncates_at_pair_boundary() { | ||||||||||||||||||||
| // Build enough fixed-shape entries to exceed MAX_INHERITED_ENV_BYTES. | ||||||||||||||||||||
| // Each entry is " K{i:08}=" (10 chars) + 100-char value = 110 bytes. | ||||||||||||||||||||
| const KEY_WIDTH: usize = 8; | ||||||||||||||||||||
| const VAL_WIDTH: usize = 100; | ||||||||||||||||||||
| let entry_bytes = 1 + 1 + KEY_WIDTH + 1 + VAL_WIDTH; | ||||||||||||||||||||
| let n = MAX_INHERITED_ENV_BYTES / entry_bytes + 2; | ||||||||||||||||||||
| let value = "v".repeat(VAL_WIDTH); | ||||||||||||||||||||
| let pairs: Vec<(String, String)> = (0..n) | ||||||||||||||||||||
| .map(|i| (format!("K{i:0width$}", width = KEY_WIDTH), value.clone())) | ||||||||||||||||||||
| .collect(); | ||||||||||||||||||||
| let out = serialize_env(pairs); | ||||||||||||||||||||
| assert!(out.len() <= MAX_INHERITED_ENV_BYTES); | ||||||||||||||||||||
| assert!( | ||||||||||||||||||||
| out.len() > MAX_INHERITED_ENV_BYTES - entry_bytes, | ||||||||||||||||||||
| "expected near-full buffer, got {} of {}", | ||||||||||||||||||||
| out.len(), | ||||||||||||||||||||
| MAX_INHERITED_ENV_BYTES | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| // Every entry must be a complete KEY=VALUE pair — no half-written keys | ||||||||||||||||||||
| // or values from a mid-pair truncation. | ||||||||||||||||||||
| for entry in out.split(' ').filter(|s| !s.is_empty()) { | ||||||||||||||||||||
| let mut parts = entry.splitn(2, '='); | ||||||||||||||||||||
| let k = parts.next().unwrap(); | ||||||||||||||||||||
| let v = parts.next().expect("entry must contain '='"); | ||||||||||||||||||||
| assert_eq!(k.len(), 1 + KEY_WIDTH, "key truncated: {k:?}"); | ||||||||||||||||||||
| assert_eq!(v.len(), VAL_WIDTH, "value truncated: {v:?}"); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[test] | ||||||||||||||||||||
| fn serialize_env_skips_oversize_but_keeps_smaller_following_vars() { | ||||||||||||||||||||
| // An oversize var must be dropped individually — not stop processing | ||||||||||||||||||||
| // — so smaller vars that come after it still land in the output. | ||||||||||||||||||||
| let oversize_value = "v".repeat(MAX_INHERITED_ENV_BYTES + 1); | ||||||||||||||||||||
| let pairs = vec![ | ||||||||||||||||||||
| ("BIG".to_string(), oversize_value), | ||||||||||||||||||||
| ("HOME".to_string(), "/root".to_string()), | ||||||||||||||||||||
| ]; | ||||||||||||||||||||
| let out = serialize_env(pairs); | ||||||||||||||||||||
| assert_eq!(out, " HOME=/root"); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| use macros::{guest, host}; | ||
|
|
||
| pub struct TestExecNullEnvp; | ||
|
|
||
| #[host] | ||
| mod host { | ||
| use super::*; | ||
|
|
||
| use crate::common::setup_fs_and_enter_with_envp; | ||
| use crate::{krun_call, krun_call_u32}; | ||
| use crate::{Test, TestSetup}; | ||
| use krun_sys::*; | ||
|
|
||
| impl Test for TestExecNullEnvp { | ||
| fn start_vm(self: Box<Self>, test_setup: TestSetup) -> anyhow::Result<()> { | ||
| unsafe { | ||
| krun_call!(krun_set_log_level(KRUN_LOG_LEVEL_TRACE))?; | ||
| let ctx = krun_call_u32!(krun_create_ctx())?; | ||
| krun_call!(krun_set_vm_config(ctx, 1, 256))?; | ||
| setup_fs_and_enter_with_envp(ctx, test_setup, std::ptr::null())?; | ||
| } | ||
| Ok(()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[guest] | ||
| mod guest { | ||
| use super::*; | ||
| use crate::Test; | ||
|
|
||
| impl Test for TestExecNullEnvp { | ||
| fn in_guest(self: Box<Self>) { | ||
| println!("OK"); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here it's not
Linux that has a 2048 limit- the limit depends on the architectureThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that is misleading, even if we keep a single limit (not platform specific), this comments needs to be updated.