Skip to content

Commit 0add974

Browse files
authored
fix(macos): enable voidbox on Apple Silicon via entitlement + kernel … (#6)
* fix(macos): enable voidbox on Apple Silicon via entitlement + kernel decompression - Add Cargo runner to auto-codesign voidbox before run (required for VZ) - Decompress gzip kernel in OCI guest images for VZ on macOS ARM64 - Update README with macOS guest-image workflow instructions * fix clippy errors
1 parent 5435c98 commit 0add974

File tree

5 files changed

+97
-4
lines changed

5 files changed

+97
-4
lines changed

.cargo/config.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,10 @@ linker = "aarch64-linux-musl-gcc"
33

44
[target.x86_64-unknown-linux-musl]
55
linker = "x86_64-linux-musl-gcc"
6+
7+
# macOS: codesign voidbox before run (required for Virtualization.framework)
8+
[target.aarch64-apple-darwin]
9+
runner = "scripts/run_voidbox_macos.sh"
10+
11+
[target.x86_64-apple-darwin]
12+
runner = "scripts/run_voidbox_macos.sh"

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,18 @@ target/debug/examples/ollama_local
301301

302302
> **Note:** Every `cargo build` invalidates the code signature. Re-run `codesign` after each rebuild.
303303
304+
When using the `voidbox` CLI, `cargo run` automatically codesigns before executing (via `.cargo/config.toml` runner). Just run:
305+
306+
```bash
307+
cargo run --bin voidbox -- run --file examples/specs/oci/guest-image-workflow.yaml
308+
```
309+
310+
If running the binary directly (e.g. `./target/debug/voidbox`), codesign manually first:
311+
312+
```bash
313+
codesign --force --sign - --entitlements voidbox.entitlements target/debug/voidbox
314+
```
315+
304316
### Parallel pipeline with per-box models
305317

306318
```bash
@@ -399,7 +411,7 @@ More OCI examples in [`examples/specs/oci/`](examples/specs/oci/):
399411
| `workflow.yaml` | Workflow with `sandbox.image: alpine:3.20` (no LLM) |
400412
| `pipeline.yaml` | Multi-language pipeline: Python base + Go and Java OCI skills |
401413
| `skills.yaml` | OCI skills only (Python, Go, Java) mounted into default initramfs |
402-
| `guest-image-workflow.yaml` | Workflow using `sandbox.guest_image` for auto-pulled kernel + initramfs |
414+
| `guest-image-workflow.yaml` | Workflow using `sandbox.guest_image` for auto-pulled kernel + initramfs (on macOS, codesign required; gzip kernel is auto-decompressed for VZ) |
403415

404416
---
405417

scripts/run_voidbox_macos.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
# Cargo runner for voidbox: on macOS, codesigns the binary before executing.
3+
# Required because Apple's Virtualization.framework needs com.apple.security.virtualization.
4+
#
5+
# Used via .cargo/config.toml [[bin]] runner for voidbox.
6+
7+
set -euo pipefail
8+
BINARY="$1"
9+
shift
10+
11+
if [[ "$(uname -s)" == "Darwin" ]]; then
12+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
13+
ENTITLEMENTS="${ROOT}/voidbox.entitlements"
14+
if [[ -f "$ENTITLEMENTS" ]]; then
15+
codesign --force --sign - --entitlements "$ENTITLEMENTS" "$BINARY" 2>/dev/null || true
16+
fi
17+
fi
18+
19+
exec "$BINARY" "$@"

voidbox-oci/src/lib.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,16 @@ impl OciClient {
148148

149149
if blob_cache.has_guest(&cache_key) {
150150
let guest_dir = blob_cache.guest_path(&cache_key);
151-
info!(path = %guest_dir.display(), "using cached guest files");
152-
return Ok(GuestImageFiles {
151+
let cached = unpack::GuestFiles {
153152
kernel: guest_dir.join("vmlinuz"),
154153
initramfs: guest_dir.join("rootfs.cpio.gz"),
154+
};
155+
// On macOS ARM64, decompress gzip kernel if vmlinux not yet present.
156+
let guest = unpack::ensure_kernel_uncompressed_for_vz(&cached)?;
157+
info!(path = %guest_dir.display(), "using cached guest files");
158+
return Ok(GuestImageFiles {
159+
kernel: guest.kernel,
160+
initramfs: guest.initramfs,
155161
});
156162
}
157163

@@ -172,6 +178,14 @@ impl OciClient {
172178
.await
173179
.map_err(|e| OciError::Layer(format!("guest extract task panicked: {}", e)))??;
174180

181+
// On macOS ARM64, VZ requires uncompressed kernel; decompress if gzip.
182+
let guest =
183+
tokio::task::spawn_blocking(move || unpack::ensure_kernel_uncompressed_for_vz(&guest))
184+
.await
185+
.map_err(|e| {
186+
OciError::Layer(format!("kernel decompress task panicked: {}", e))
187+
})??;
188+
175189
blob_cache.mark_guest_done(&cache_key).await?;
176190

177191
info!(

voidbox-oci/src/unpack.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub fn unpack_layers(layers: &[LayerInfo], dest: &Path) -> Result<PathBuf> {
3030
}
3131

3232
/// Paths to the extracted guest files (kernel + initramfs).
33-
#[derive(Debug)]
33+
#[derive(Clone, Debug)]
3434
pub struct GuestFiles {
3535
pub kernel: PathBuf,
3636
pub initramfs: PathBuf,
@@ -99,6 +99,47 @@ pub fn extract_guest_files(layers: &[LayerInfo], dest: &Path) -> Result<GuestFil
9999
})
100100
}
101101

102+
/// If the kernel is gzip-compressed and we're on macOS ARM64 (VZ backend),
103+
/// decompress it to vmlinux. Apple's Virtualization.framework requires
104+
/// uncompressed ARM64 kernels. Returns the path to use for the kernel.
105+
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
106+
pub fn ensure_kernel_uncompressed_for_vz(guest: &GuestFiles) -> Result<GuestFiles> {
107+
let kernel_bytes =
108+
fs::read(&guest.kernel).map_err(|e| OciError::Layer(format!("read kernel: {}", e)))?;
109+
110+
// Gzip magic bytes (RFC 1952): 0x1f 0x8b
111+
if kernel_bytes.len() < 2 || kernel_bytes[..2] != [0x1f, 0x8b] {
112+
return Ok(guest.clone());
113+
}
114+
115+
info!(
116+
path = %guest.kernel.display(),
117+
"kernel is gzip-compressed; decompressing for VZ (required on macOS ARM64)",
118+
);
119+
120+
let decompressed_path = guest.kernel.parent().unwrap().join("vmlinux");
121+
let mut decoder = GzDecoder::new(&kernel_bytes[..]);
122+
let mut decompressed = Vec::new();
123+
decoder
124+
.read_to_end(&mut decompressed)
125+
.map_err(|e| OciError::Layer(format!("decompress kernel: {}", e)))?;
126+
127+
fs::write(&decompressed_path, &decompressed)
128+
.map_err(|e| OciError::Layer(format!("write vmlinux: {}", e)))?;
129+
130+
info!(path = %decompressed_path.display(), "decompressed kernel ready");
131+
132+
Ok(GuestFiles {
133+
kernel: decompressed_path,
134+
initramfs: guest.initramfs.clone(),
135+
})
136+
}
137+
138+
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
139+
pub fn ensure_kernel_uncompressed_for_vz(guest: &GuestFiles) -> Result<GuestFiles> {
140+
Ok(guest.clone())
141+
}
142+
102143
// ---------------------------------------------------------------------------
103144
// Single layer extraction
104145
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)