From 0f875f8c9cbbf9cf40f052d7a529c5af9e609dde Mon Sep 17 00:00:00 2001 From: Matteo Chesi Date: Thu, 23 Apr 2026 18:31:14 +0200 Subject: [PATCH 1/5] Add sethomevar hook --- .github/workflows/release.yml | 1 + Cargo.toml | 2 +- crates/sethomevar/Cargo.toml | 10 ++ crates/sethomevar/README.md | 28 +++++ crates/sethomevar/src/main.rs | 212 ++++++++++++++++++++++++++++++++++ 5 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 crates/sethomevar/Cargo.toml create mode 100644 crates/sethomevar/README.md create mode 100644 crates/sethomevar/src/main.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89cd092..9517299 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,6 +58,7 @@ jobs: cp target/release/ldcache_hook "target/release/ldcache_hook-${{ matrix.arch }}" cp target/release/pce_hook "target/release/pce_hook-${{ matrix.arch }}" cp target/release/mps_hook "target/release/mps_hook-${{ matrix.arch }}" + cp target/release/sethomevar "target/release/sethomevar-${{ matrix.arch }}" ls -lah target/release/ ' diff --git a/Cargo.toml b/Cargo.toml index e40bdce..32145b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/pce_hook", "crates/mps_hook", "crates/ldcache_hook"] +members = ["crates/pce_hook", "crates/mps_hook", "crates/ldcache_hook", "crates/sethomevar"] resolver = "2" [profile.release] diff --git a/crates/sethomevar/Cargo.toml b/crates/sethomevar/Cargo.toml new file mode 100644 index 0000000..020c75a --- /dev/null +++ b/crates/sethomevar/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sethomevar" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +users = "0.11.0" diff --git a/crates/sethomevar/README.md b/crates/sethomevar/README.md new file mode 100644 index 0000000..5a096b3 --- /dev/null +++ b/crates/sethomevar/README.md @@ -0,0 +1,28 @@ +# Set HOME variable - Precreate Hook + +Update container environment replacing HOME variable for running user with the one from the host system. + +**What it does** + +* Reads the **container config JSON** from `stdin` and emits the updated config to `stdout`. +* Reads running user uid from container config. +* Find HOME host value via getpwuid_r +* Replace HOME entry in container config env +* Pretty-prints output and exits non-zero on validation/parse errors (errors go to `stderr`). + +## Usage as a Podman hook + +Add a createContainer hook entry similar to: + +```json +{ + "version": "1.0.0", + "hook": { + "path": "/opt/hooks/sethomevar" + }, + "when": { + "always": true + }, + "stages": ["precreate"] +} +``` diff --git a/crates/sethomevar/src/main.rs b/crates/sethomevar/src/main.rs new file mode 100644 index 0000000..1d35598 --- /dev/null +++ b/crates/sethomevar/src/main.rs @@ -0,0 +1,212 @@ +use std::{ + io::{self, Read, Write}, + process, +}; + +use serde_json::{Map, Value, json}; +use users::get_user_by_uid; +use users::os::unix::UserExt; + +fn main() -> io::Result<()> { + // we go for run + if let Err(e) = run() { + // we output errors to stderr + eprintln!("{e}"); + + // we return failure status + process::exit(1); + } + + Ok(()) +} + +fn run() -> Result<(), String> { + // Read and parse stdin JSON + let mut value = read_stdin_json()?; + let obj = ensure_obj(value.as_object_mut(), "top-level JSON must be an object")?; + + //let env_entries_raw = vec![String::from("HOME=/stikazzi")]; + let env_entries_raw = vec![get_home_env_entry(obj)?]; + + /* + let (mounts_to_add, env_entries_raw) = read_pce_input()?; + + if !mounts_to_add.is_empty() { + append_mounts(obj, mounts_to_add)?; + } + */ + + // Validate env entries and merge as strings + let env_entries = validate_env_strings(env_entries_raw)?; + if !env_entries.is_empty() { + merge_process_env_strings(obj, env_entries)?; + } + + // Pretty-print output JSON with trailing newline + let mut stdout = io::stdout().lock(); + + serde_json::to_writer_pretty(&mut stdout, &value) + .map_err(|e| format!("Failed to write JSON to stdout: {e}"))?; + + stdout + .write_all(b"\n") + .map_err(|e| format!("Failed to write newline to stdout: {e}"))?; + stdout + .flush() + .map_err(|e| format!("Failed to flush stdout: {e}"))?; + + Ok(()) +} + +// Returning ENV HOME entry from the system /etc/passwd +// 1. get process.user.uid entry from json obj +// 2. get user entry from uid through getpwuid_r +// 3. get homedir from user entry +// 4. build HOME entry string and return it +fn get_home_env_entry(obj: &mut Map) -> Result { + // Ensure "process" is an object + let process_val = obj + .entry("process".to_string()) + .or_insert_with(|| json!({})); + let process_obj = process_val + .as_object_mut() + .ok_or_else(|| "Validation error: 'process' exists but is not an object.".to_string())?; + + // Ensure "user" is an object + let user_val = process_obj + .entry("user".to_string()) + .or_insert_with(|| json!({})); + let user_obj = user_val + .as_object_mut() + .ok_or_else(|| "Validation error: 'user' exists but is not an object.".to_string())?; + + // Ensure "uid" is a number + let uid: u32 = user_obj + .entry("uid".to_string()) + .or_insert_with(|| json!(0)) + .as_number() + .ok_or_else(|| "Validation error: 'uid' exists but is not a number.".to_string())? + .as_u64() + .ok_or_else(|| "Validation error: 'uid' is a number but doesn't fit u64.".to_string())? + .try_into() + .map_err(|e| format!("Validation error: 'uid' is a number but doesn't fit u32: {e}"))?; + + let user = get_user_by_uid(uid) + .ok_or_else(|| "Unknown UID: cannot find User by UID {uid}".to_string()) + .unwrap(); + let homedir = user.home_dir().display(); + + let home_env_entry = format!("HOME={homedir}"); + + Ok(home_env_entry) +} + +// Precreate takes as stdin the container config json +// We return error if we cannot read or +// if we cannot parse a valid input json +fn read_stdin_json() -> Result { + let mut input = String::new(); + + io::stdin() + .read_to_string(&mut input) + .map_err(|e| format!("Failed to read from stdin: {e}"))?; + + serde_json::from_str(&input).map_err(|e| format!("Invalid JSON: {e}")) +} + +/// Ensure a `Value` is an object and return it as a mutable map. +fn ensure_obj<'a>( + candidate: Option<&'a mut Map>, + err: &str, +) -> Result<&'a mut Map, String> { + candidate.ok_or_else(|| format!("Validation error: {err}.")) +} + +fn ensure_array_field<'a>( + obj: &'a mut Map, + field: &str, +) -> Result<&'a mut Vec, String> { + use serde_json::map::Entry; + + // before we return the field, we check if the entry is empty/vacant, if so we create the + // field, otherwise, we check it needs to be an array or we return error. + // TODO: can we have non-array env and mounts? + match obj.entry(field.to_string()) { + Entry::Vacant(v) => { + // Insert an empty array and return a mutable ref to it. + let val = v.insert(Value::Array(Vec::new())); + Ok(val.as_array_mut().expect("we just inserted an Array")) + } + Entry::Occupied(e) => { + // Tie the borrow to `obj` by consuming the entry. + let v = e.into_mut(); // &'a mut Value + match v { + Value::Array(arr) => Ok(arr), + _ => Err(format!( + "Validation error: '{field}' exists but is not an array." + )), + } + } + } +} + +/// Validate a list of "KEY=value" strings. +fn validate_env_strings(entries: Vec) -> Result, String> { + for s in &entries { + validate_kv_format(s)?; + } + + Ok(entries) +} + +fn validate_kv_format(s: &str) -> Result<(), String> { + if let Some((k, _v)) = s.split_once('=') { + if k.is_empty() { + return Err("Empty environment variable name before '='".into()); + } + Ok(()) + } else { + Err(format!("Invalid env entry (expected KEY=VALUE): {s}")) + } +} + +// merging envs into the container config json is as follows +// 1. we need to add envs into the process object +// 1.5 we create process if it is not there +// 2. we validate out envs +// 3 new env entries are added using two rules +// 3.1 we append if the env var is new +// 3.2 we replace if we find it duplicated +fn merge_process_env_strings( + obj: &mut Map, + env_entries: Vec, +) -> Result<(), String> { + // Ensure "process" is an object + let process_val = obj + .entry("process".to_string()) + .or_insert_with(|| json!({})); + let process_obj = process_val + .as_object_mut() + .ok_or_else(|| "Validation error: 'process' exists but is not an object.".to_string())?; + + let env_arr = ensure_array_field(process_obj, "env")?; + + // logic to add new envs + for new in env_entries { + // Safe: already validated as KEY=value in main + let (new_key, _) = new.split_once('=').unwrap(); + + // We scan to find if we have a duplicate, if so we overwrite with new + if let Some(idx) = env_arr.iter().rposition(|v| { + v.as_str() + .and_then(|s| s.split_once('=').map(|(k, _)| k)) + .is_some_and(|k| k == new_key) + }) { + env_arr[idx] = Value::String(new); + } else { + env_arr.push(Value::String(new)); + } + } + + Ok(()) +} From 66171ca44a035cfc9869c044b91a9200540649df Mon Sep 17 00:00:00 2001 From: Matteo Chesi Date: Thu, 23 Apr 2026 19:03:35 +0200 Subject: [PATCH 2/5] Clean-up --- crates/sethomevar/src/main.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/sethomevar/src/main.rs b/crates/sethomevar/src/main.rs index 1d35598..6a384ec 100644 --- a/crates/sethomevar/src/main.rs +++ b/crates/sethomevar/src/main.rs @@ -25,17 +25,8 @@ fn run() -> Result<(), String> { let mut value = read_stdin_json()?; let obj = ensure_obj(value.as_object_mut(), "top-level JSON must be an object")?; - //let env_entries_raw = vec![String::from("HOME=/stikazzi")]; let env_entries_raw = vec![get_home_env_entry(obj)?]; - /* - let (mounts_to_add, env_entries_raw) = read_pce_input()?; - - if !mounts_to_add.is_empty() { - append_mounts(obj, mounts_to_add)?; - } - */ - // Validate env entries and merge as strings let env_entries = validate_env_strings(env_entries_raw)?; if !env_entries.is_empty() { From fa7d9b12781b4ee5baa79f744da23c02552eee45 Mon Sep 17 00:00:00 2001 From: matteo-chesi Date: Fri, 1 May 2026 08:03:37 +0200 Subject: [PATCH 3/5] Apply suggestion from @Madeeks Co-authored-by: Alberto Madonna --- crates/sethomevar/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/sethomevar/README.md b/crates/sethomevar/README.md index 5a096b3..0bb7382 100644 --- a/crates/sethomevar/README.md +++ b/crates/sethomevar/README.md @@ -12,7 +12,7 @@ Update container environment replacing HOME variable for running user with the o ## Usage as a Podman hook -Add a createContainer hook entry similar to: +Add a `precreate` hook entry similar to: ```json { From 36f2009552aef11aa1da04109addfa35b3cbf19a Mon Sep 17 00:00:00 2001 From: Matteo Chesi Date: Mon, 4 May 2026 15:16:09 +0200 Subject: [PATCH 4/5] return error on missing process.user.uid --- crates/sethomevar/src/main.rs | 44 ++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/crates/sethomevar/src/main.rs b/crates/sethomevar/src/main.rs index 6a384ec..19ac43e 100644 --- a/crates/sethomevar/src/main.rs +++ b/crates/sethomevar/src/main.rs @@ -3,7 +3,7 @@ use std::{ process, }; -use serde_json::{Map, Value, json}; +use serde_json::{Map, map::Entry, Value, json}; use users::get_user_by_uid; use users::os::unix::UserExt; @@ -55,32 +55,48 @@ fn run() -> Result<(), String> { // 3. get homedir from user entry // 4. build HOME entry string and return it fn get_home_env_entry(obj: &mut Map) -> Result { + + // Ensure "process" exists + let process_val = obj.entry("process".to_string()); + match process_val { + Entry::Vacant(_) => return Err(format!("Validation error: 'process' doesn't exist.")), + Entry::Occupied(_) => {}, + } + // Ensure "process" is an object - let process_val = obj - .entry("process".to_string()) - .or_insert_with(|| json!({})); let process_obj = process_val + .or_insert_with(|| json!({})) .as_object_mut() .ok_or_else(|| "Validation error: 'process' exists but is not an object.".to_string())?; - // Ensure "user" is an object - let user_val = process_obj - .entry("user".to_string()) - .or_insert_with(|| json!({})); + // Ensure "user" exists + let user_val = process_obj.entry("user".to_string()); + match user_val { + Entry::Vacant(_) => return Err(format!("Validation error: 'process.user' doesn't exist.")), + Entry::Occupied(_) => {}, + } + let user_obj = user_val + .or_insert_with(|| json!({})) .as_object_mut() - .ok_or_else(|| "Validation error: 'user' exists but is not an object.".to_string())?; + .ok_or_else(|| "Validation error: 'process.user' exists but is not an object.".to_string())?; + + // Ensure "uid" exists + let uid_val = user_obj.entry("uid".to_string()); + match uid_val { + Entry::Vacant(_) => return Err(format!("Validation error: 'process.user.uid' doesn't exist.")), + Entry::Occupied(_) => {}, + } // Ensure "uid" is a number - let uid: u32 = user_obj - .entry("uid".to_string()) + let uid: u32 = uid_val .or_insert_with(|| json!(0)) .as_number() - .ok_or_else(|| "Validation error: 'uid' exists but is not a number.".to_string())? + .ok_or_else(|| "Validation error: 'process.user.uid' exists but is not a number.".to_string())? .as_u64() - .ok_or_else(|| "Validation error: 'uid' is a number but doesn't fit u64.".to_string())? + .ok_or_else(|| "Validation error: 'process.user.uid' is a number but doesn't fit u64.".to_string())? .try_into() - .map_err(|e| format!("Validation error: 'uid' is a number but doesn't fit u32: {e}"))?; + .map_err(|e| format!("Validation error: 'process.user.uid' is a number but doesn't fit u32: {e}"))?; let user = get_user_by_uid(uid) .ok_or_else(|| "Unknown UID: cannot find User by UID {uid}".to_string()) From 73d2817f3de1ba201e081929aaa4d31226fac244 Mon Sep 17 00:00:00 2001 From: Matteo Chesi Date: Tue, 5 May 2026 11:10:13 +0200 Subject: [PATCH 5/5] downgrade edition from 2024 to 2021 --- crates/sethomevar/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/sethomevar/Cargo.toml b/crates/sethomevar/Cargo.toml index 020c75a..2048c19 100644 --- a/crates/sethomevar/Cargo.toml +++ b/crates/sethomevar/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sethomevar" version = "0.1.0" -edition = "2024" +edition = "2021" publish = false [dependencies]