@@ -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