Skip to content

Commit 6b706ca

Browse files
authored
Merge pull request #50 from AI45Lab/fix/box-exec-arg-b64
fix(box): base64-encode exec args/env so quotes survive libkrun env passing
2 parents 8589a5d + 8a8bb57 commit 6b706ca

2 files changed

Lines changed: 69 additions & 21 deletions

File tree

src/guest/init/src/main.rs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -247,31 +247,59 @@ impl ExecConfig {
247247
// (runtime/src/vm/spec.rs), so this default is only a defensive fallback.
248248
// Use /bin/sh — universal across distros — never /sbin/init, which does
249249
// not exist on Alpine and was the original cause of issue #3.
250-
let executable = std::env::var("BOX_EXEC_EXEC").unwrap_or_else(|_| "/bin/sh".to_string());
250+
// BOX_EXEC_* values are base64-encoded (URL-safe, no pad) by the runtime
251+
// when BOX_EXEC_B64=1, so arbitrary bytes (quotes, spaces, `$`, …) survive
252+
// libkrun's env serialization. Decode them back; fall back to the raw value
253+
// on any decode error or when the marker is absent (older runtime).
254+
use base64::Engine;
255+
let b64 = std::env::var("BOX_EXEC_B64")
256+
.map(|v| v == "1")
257+
.unwrap_or(false);
258+
let decode = |s: String| -> String {
259+
if !b64 {
260+
return s;
261+
}
262+
base64::engine::general_purpose::URL_SAFE_NO_PAD
263+
.decode(s.as_bytes())
264+
.ok()
265+
.and_then(|bytes| String::from_utf8(bytes).ok())
266+
.unwrap_or(s)
267+
};
268+
269+
let executable = std::env::var("BOX_EXEC_EXEC")
270+
.map(&decode)
271+
.unwrap_or_else(|_| "/bin/sh".to_string());
251272

252273
// Parse args from individual env vars (BOX_EXEC_ARGC + BOX_EXEC_ARG_0..N)
253274
let args: Vec<String> = match std::env::var("BOX_EXEC_ARGC")
254275
.ok()
255276
.and_then(|s| s.parse::<usize>().ok())
256277
{
257278
Some(argc) => (0..argc)
258-
.filter_map(|i| std::env::var(format!("BOX_EXEC_ARG_{}", i)).ok())
279+
.filter_map(|i| {
280+
std::env::var(format!("BOX_EXEC_ARG_{}", i))
281+
.ok()
282+
.map(&decode)
283+
})
259284
.collect(),
260285
None => vec![],
261286
};
262287

263-
let workdir = std::env::var("BOX_EXEC_WORKDIR").unwrap_or_else(|_| "/".to_string());
288+
let workdir = std::env::var("BOX_EXEC_WORKDIR")
289+
.map(&decode)
290+
.unwrap_or_else(|_| "/".to_string());
264291

265292
// Optional container user (image USER directive or CLI --user).
266293
let user = std::env::var("BOX_EXEC_USER")
267294
.ok()
295+
.map(&decode)
268296
.filter(|u| !u.is_empty());
269297

270-
// Collect BOX_EXEC_ENV_* variables
298+
// Collect BOX_EXEC_ENV_* variables (values decoded as above).
271299
let env: Vec<(String, String)> = std::env::vars()
272300
.filter_map(|(key, value)| {
273301
key.strip_prefix("BOX_EXEC_ENV_")
274-
.map(|stripped| (stripped.to_string(), value))
302+
.map(|stripped| (stripped.to_string(), decode(value)))
275303
})
276304
.collect();
277305

src/runtime/src/vm/spec.rs

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,20 @@ impl VmManager {
134134
};
135135
a3s_box_core::env::merge_env_pairs(&mut container_env, &self.config.extra_env);
136136

137-
// Pass exec + args as individual env vars (avoids spaces being truncated
138-
// by libkrun's env serialization).
137+
// Pass exec + args as individual env vars, base64-encoded (URL-safe, no
138+
// padding) so arbitrary bytes — spaces, quotes, `$`, `;` — survive
139+
// libkrun's env serialization intact (a raw `"` in a value was being
140+
// corrupted). `BOX_EXEC_B64=1` tells guest-init to decode them.
141+
use base64::Engine;
142+
let b64 =
143+
|s: &str| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(s.as_bytes());
139144
let mut env: Vec<(String, String)> = vec![
140-
("BOX_EXEC_EXEC".to_string(), exec),
145+
("BOX_EXEC_B64".to_string(), "1".to_string()),
146+
("BOX_EXEC_EXEC".to_string(), b64(&exec)),
141147
("BOX_EXEC_ARGC".to_string(), args.len().to_string()),
142148
];
143149
for (i, arg) in args.iter().enumerate() {
144-
env.push((format!("BOX_EXEC_ARG_{}", i), arg.clone()));
150+
env.push((format!("BOX_EXEC_ARG_{}", i), b64(arg)));
145151
}
146152

147153
// Prototype: deferred-main-spawn. If the host set BOX_DEFERRED_MAIN=1,
@@ -157,20 +163,23 @@ impl VmManager {
157163

158164
// Pass the effective working directory to guest init so PID 1 and
159165
// the container entrypoint agree even when no OCI WORKDIR is set.
160-
env.push(("BOX_EXEC_WORKDIR".to_string(), workdir.clone()));
166+
env.push(("BOX_EXEC_WORKDIR".to_string(), b64(&workdir)));
161167

162168
// Pass the container user (image USER / --user) to guest init, which
163169
// applies it (setgroups+setgid+setuid) to the MAIN process right
164170
// before exec — after PID 1 has done its root-only setup. This must
165171
// NOT go through the shim's libkrun set_uid, which would drop guest
166172
// PID 1 to the user and break init (mount/chroot need root).
167173
if let Some(user) = &user {
168-
env.push(("BOX_EXEC_USER".to_string(), user.clone()));
174+
env.push(("BOX_EXEC_USER".to_string(), b64(user)));
169175
}
170176

171-
// Pass container environment variables with BOX_EXEC_ENV_ prefix
177+
// Pass container environment variables with BOX_EXEC_ENV_ prefix (values
178+
// base64-encoded like the rest, so a value containing `"`/spaces/etc.
179+
// reaches the container intact). The key stays raw — env names are
180+
// restricted to a safe character set.
172181
for (key, value) in container_env {
173-
env.push((format!("BOX_EXEC_ENV_{}", key), value));
182+
env.push((format!("BOX_EXEC_ENV_{}", key), b64(&value)));
174183
}
175184

176185
// Pass user volume mounts to guest init for mounting inside the VM.
@@ -747,6 +756,17 @@ mod tests {
747756
use tempfile::tempdir;
748757
use tempfile::TempDir;
749758

759+
/// Decode a base64 (URL-safe, no pad) `BOX_EXEC_*` value the way guest-init
760+
/// does, so assertions can compare against the original raw value.
761+
fn b64d(s: &str) -> String {
762+
use base64::Engine;
763+
base64::engine::general_purpose::URL_SAFE_NO_PAD
764+
.decode(s.as_bytes())
765+
.ok()
766+
.and_then(|bytes| String::from_utf8(bytes).ok())
767+
.unwrap_or_else(|| s.to_string())
768+
}
769+
750770
fn test_oci_config(workdir: Option<&str>, user: Option<&str>) -> OciImageConfig {
751771
OciImageConfig {
752772
entrypoint: Some(vec!["/bin/app".to_string()]),
@@ -1065,12 +1085,12 @@ mod tests {
10651085
.entrypoint
10661086
.env
10671087
.iter()
1068-
.any(|(key, value)| key == "BOX_EXEC_USER" && value == "1000:1000"));
1088+
.any(|(key, value)| key == "BOX_EXEC_USER" && b64d(value) == "1000:1000"));
10691089
assert!(spec
10701090
.entrypoint
10711091
.env
10721092
.iter()
1073-
.any(|(key, value)| key == "BOX_EXEC_WORKDIR" && value == "/override"));
1093+
.any(|(key, value)| key == "BOX_EXEC_WORKDIR" && b64d(value) == "/override"));
10741094
}
10751095

10761096
#[test]
@@ -1117,12 +1137,12 @@ mod tests {
11171137
.entrypoint
11181138
.env
11191139
.iter()
1120-
.any(|(key, value)| key == "BOX_EXEC_USER" && value == "2000:2000"));
1140+
.any(|(key, value)| key == "BOX_EXEC_USER" && b64d(value) == "2000:2000"));
11211141
assert!(spec
11221142
.entrypoint
11231143
.env
11241144
.iter()
1125-
.any(|(key, value)| key == "BOX_EXEC_WORKDIR" && value == "/oci"));
1145+
.any(|(key, value)| key == "BOX_EXEC_WORKDIR" && b64d(value) == "/oci"));
11261146
}
11271147

11281148
#[test]
@@ -1138,7 +1158,7 @@ mod tests {
11381158
.entrypoint
11391159
.env
11401160
.iter()
1141-
.any(|(key, value)| key == "BOX_EXEC_WORKDIR" && value == GUEST_WORKDIR));
1161+
.any(|(key, value)| key == "BOX_EXEC_WORKDIR" && b64d(value) == GUEST_WORKDIR));
11421162
}
11431163

11441164
#[test]
@@ -1182,17 +1202,17 @@ mod tests {
11821202
.entrypoint
11831203
.env
11841204
.iter()
1185-
.any(|(key, value)| key == "BOX_EXEC_ENV_FOO" && value == "cli"));
1205+
.any(|(key, value)| key == "BOX_EXEC_ENV_FOO" && b64d(value) == "cli"));
11861206
assert!(spec
11871207
.entrypoint
11881208
.env
11891209
.iter()
1190-
.any(|(key, value)| key == "BOX_EXEC_ENV_BAR" && value == "image"));
1210+
.any(|(key, value)| key == "BOX_EXEC_ENV_BAR" && b64d(value) == "image"));
11911211
assert!(spec
11921212
.entrypoint
11931213
.env
11941214
.iter()
1195-
.any(|(key, value)| key == "BOX_EXEC_ENV_BAZ" && value == "cli"));
1215+
.any(|(key, value)| key == "BOX_EXEC_ENV_BAZ" && b64d(value) == "cli"));
11961216
assert!(!spec
11971217
.entrypoint
11981218
.env

0 commit comments

Comments
 (0)