diff --git a/.github/workflows/kernel.yml b/.github/workflows/kernel.yml index 315a21675..16209f9e0 100644 --- a/.github/workflows/kernel.yml +++ b/.github/workflows/kernel.yml @@ -1,15 +1,9 @@ name: kernel -# Builds each guest kernel twice and proves the hashes match the ones shard was built with. A PR that -# touches the build runs that much; a manual dispatch also publishes under the tag services/kernel names. +# Builds each guest kernel twice, proves the hashes match the ones shard was built with, and publishes +# under the tag services/kernel names. Manual only: the kernel changes rarely, and each build takes 13 minutes. on: workflow_dispatch: - pull_request: - paths: - - Makefile - - packaging/kernel/** - - services/kernel/** - - .github/workflows/kernel.yml permissions: contents: read diff --git a/.gitignore b/.gitignore index 699fec788..879945a96 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,5 @@ coverage.* # Local Claude Code worktrees, one per in-flight ticket /.claude/worktrees/ -# The shim binary is build output; pkg/vz embeds it from here -/pkg/vz/shim/shard-vz-shim +# The shim binary is build output; pkg/vzshim embeds it from here +/pkg/vzshim/shim/shard-vz-shim diff --git a/.golangci.yml b/.golangci.yml index 7c82410ff..8497ec4ee 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -49,6 +49,7 @@ linters: - $gostd - github.com/presmihaylov/shard/models - github.com/presmihaylov/shard/pkg/pty + - github.com/presmihaylov/shard/pkg/vzshim - github.com/presmihaylov/shard/services/client - github.com/presmihaylov/shard/services/sandbox - github.com/presmihaylov/shard/services/daemon diff --git a/AGENTS.md b/AGENTS.md index c4087c749..f336fc310 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,8 @@ make build build ./cmd/shard into bin/shard make build-linux cross-compile for the box (GOOS=linux GOARCH=amd64) make build-shard-init build ./cmd/shard-init into bin/shard-init (static, CGO_ENABLED=0) make build-shard-init-linux cross-compile the supervisor for the box -make build-shard-vz-shim build and ad-hoc sign the VM shim into bin/shard-vz-shim (darwin only) +make build-shard-vz-shim build and ad-hoc sign the VM shim into pkg/vzshim/shim, where the daemon embeds it (darwin only) +make build-darwin the shim, then ./cmd/shard with cgo for this Mac, into bin/shard-darwin- make test unit tests; must stay green on macOS make test-integration integration tests, on this host; Linux box only, needs root make itest integration tests for ITEST_PKG, on the devbox @@ -60,6 +61,8 @@ pkg/registry/ OCI registry transport pkg/netns/ netns, veth, bridge, NAT rules pkg/store/ atomic file write, the daemon singleton lock pkg/proxy/ intercepting HTTP and TLS proxy +pkg/vz/ the Virtualization.framework driver: the shim protocol, its client and its server +pkg/vzshim/ the shim binary embedded in the daemon, installed and ad-hoc signed on first use services/sandbox/ the orchestrator: the lifecycle verbs the daemon serves services/image/ pull, unpack, cache policy @@ -90,9 +93,9 @@ docs/ a driver and it belongs in `services/`. `depguard` enforces this in CI. - **Dependencies point one way: `cli` to `services` to `pkg`.** `models` sits under all of them. -- **`cli/` imports `services/client`, `pkg/pty`, `models`, the request types in - `services/sandbox`, and `services/daemon` and `services/serve` for the two - verbs that are a process rather than a client. Nothing else.** A verb holds no +- **`cli/` imports `services/client`, `pkg/pty`, `pkg/vzshim`, `models`, the request + types in `services/sandbox`, and `services/daemon` and `services/serve` for the + two verbs that are a process rather than a client. Nothing else.** A verb holds no store and no provider: it asks the socket. `depguard` enforces the allow list in CI. - **`models/` is one package with several files, and it is a leaf.** It imports diff --git a/Makefile b/Makefile index f5030ebfb..74f2851ce 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ BIN := bin/shard SHARD_INIT_BIN := bin/shard-init +VZ_SHIM_BIN := pkg/vzshim/shim/shard-vz-shim PKG := github.com/presmihaylov/shard VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) LDFLAGS := -X main.version=$(VERSION) @@ -19,7 +20,7 @@ ARCH ?= arm64 KERNEL_OUT := bin/kernel KERNEL_IMAGE := packaging-kernel-builder -.PHONY: all build build-linux build-shard-init build-shard-init-linux build-shard-vz-shim test test-integration e2e-test vet lint lint-fix fmt fmt-check vuln check clean devbox-sync devbox-test itest e2e devbox-e2e devbox-demo kernel kernel-reproducible +.PHONY: all build build-linux build-shard-init build-shard-init-linux build-shard-vz-shim build-darwin test test-integration e2e-test vet lint lint-fix fmt fmt-check vuln check clean devbox-sync devbox-test itest e2e devbox-e2e devbox-demo kernel kernel-reproducible all: check build @@ -38,10 +39,14 @@ build-shard-init: build-shard-init-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $(SHARD_INIT_BIN)-linux-amd64 ./cmd/shard-init -# The shim holds one Virtualization.framework VM; darwin only, and unsigned it cannot create one. +# The shim holds one Virtualization.framework VM. It lands where pkg/vzshim embeds it, signed, so a direct run works too. build-shard-vz-shim: - go build -o bin/shard-vz-shim ./cmd/shard-vz-shim - codesign --sign - --force --entitlements cmd/shard-vz-shim/entitlements.plist bin/shard-vz-shim + go build -o $(VZ_SHIM_BIN) ./cmd/shard-vz-shim + codesign --sign - --force --entitlements pkg/vzshim/shim/entitlements.plist $(VZ_SHIM_BIN) + +# The Mac build: cgo over the framework never cross-compiles, and the daemon carries the shim it will install and sign. +build-darwin: build-shard-vz-shim + CGO_ENABLED=1 go build -ldflags "$(LDFLAGS)" -o $(BIN)-darwin-$(shell go env GOARCH) ./cmd/shard test: go test ./... @@ -111,7 +116,7 @@ vuln: check: fmt-check vet lint test e2e-test clean: - rm -rf bin + rm -rf bin $(VZ_SHIM_BIN) # One guest kernel, built in the pinned amd64 image so the bytes match CI wherever it runs (SHARD-232). kernel: diff --git a/cli/cli.go b/cli/cli.go index accc00f7b..1425a9c78 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -428,8 +428,14 @@ func (a App) version(ctx context.Context) error { if err != nil { return err } + if err := a.print("daemon " + daemon.Version); err != nil { + return err + } + if line := shimLine(); line != "" { + return a.print(line) + } - return a.print("daemon " + daemon.Version) + return nil } // warn reports something the operator should know that is not a reason to fail the command. diff --git a/cli/client_test.go b/cli/client_test.go index cc23eb496..fd55a3e3d 100644 --- a/cli/client_test.go +++ b/cli/client_test.go @@ -34,7 +34,8 @@ func TestVersionPrintsBothLines(t *testing.T) { t.Fatalf("version: %v", err) } - if got := strings.TrimSpace(out.String()); got != "client test\ndaemon v-daemon" { + // A Mac binary adds a third line for the VM shim after these two. + if got := out.String(); !strings.HasPrefix(got, "client test\ndaemon v-daemon\n") { t.Errorf("version printed %q, want the client line and the daemon line", got) } } diff --git a/cli/shim_darwin.go b/cli/shim_darwin.go new file mode 100644 index 000000000..7c379f855 --- /dev/null +++ b/cli/shim_darwin.go @@ -0,0 +1,12 @@ +package cli + +import "github.com/presmihaylov/shard/pkg/vzshim" + +// shimLine says whether this binary carries the VM shim, which only make build-darwin puts there. +func shimLine() string { + if vzshim.Embedded() { + return "vz shim embedded" + } + + return "vz shim absent: run make build-darwin" +} diff --git a/cli/shim_other.go b/cli/shim_other.go new file mode 100644 index 000000000..cbab8e6ed --- /dev/null +++ b/cli/shim_other.go @@ -0,0 +1,6 @@ +//go:build !darwin + +package cli + +// Only a Mac daemon carries a VM shim, so there is no line to print elsewhere. +func shimLine() string { return "" } diff --git a/docs/macos-signing.md b/docs/macos-signing.md new file mode 100644 index 000000000..184edda5a --- /dev/null +++ b/docs/macos-signing.md @@ -0,0 +1,88 @@ +# Code signing on macOS + +Only `shard-vz-shim` needs a signature that shard makes on purpose. Apple's Virtualization.framework +refuses to create a VM for a process without the `com.apple.security.virtualization` entitlement, and +an entitlement is carried by a code signature. The shim is the only process that touches the +framework (`docs/provider-vz.md`), so it is the only binary shard signs. The daemon and the CLI are +one Go binary, `shard`, and it stays as the Go linker leaves it. + +## What ships + +`make build-darwin` builds the shim, ad-hoc signs it into `pkg/vzshim/shim/`, and then builds `shard` +with the shim embedded. `pkg/vzshim` is its own package, which the daemon links and the shim does +not: a shim that embedded its own previous build would never hash the same twice. `make clean` +removes the built shim too, so a plain `go build` after it carries none and `Install` returns +`ErrNoShim`. The vz provider (SHARD-218) calls `vzshim.Install` before its first boot: it writes +the shim into the shard root and ad-hoc signs it there, with the entitlements plist it also +embeds, in a temporary file it removes after the signature. A stamp beside the shim, +`shard-vz-shim.sha256`, holds the hash of the embedded build, so a later start finds the shim in +place, and a build with a different shim replaces the file by rename, so a running shim keeps +its inode. Concurrent callers each sign a temporary copy of their own and publish it by rename, +so the path never holds a partial file. The install needs `codesign`, which the Command Line +Tools provide; Xcode is not needed. + +The installed shim, on a Mac with only the Command Line Tools: + +``` +$ codesign -d --entitlements - /var/lib/shard/shard-vz-shim +[Dict] + [Key] com.apple.security.virtualization + [Value] + [Bool] true + +$ codesign -dvv /var/lib/shard/shard-vz-shim +Format=Mach-O thin (arm64) +CodeDirectory v=20400 size=56975 flags=0x2(adhoc) hashes=1769+7 location=embedded +Signature=adhoc +TeamIdentifier=not set +``` + +The daemon, for comparison, carries the linker's own ad-hoc signature and no entitlements. Every +arm64 Mach-O needs one to execute at all, and the Go linker adds it: + +``` +$ codesign -dvv bin/shard-darwin-arm64 +CodeDirectory v=20400 size=122014 flags=0x20002(adhoc,linker-signed) hashes=3810+0 location=embedded +Signature=adhoc +``` + +## What an ad-hoc signature can and cannot do + +An ad-hoc signature (`codesign --sign -`) seals the binary and its entitlements with no identity +behind it. On the machine where it was made it is enough: the kernel checks the seal, the framework +finds the entitlement, the VM boots. That is the whole of what shard needs on a developer Mac or a +self-managed Mac mini. + +It cannot pass Gatekeeper on another machine as a downloaded app, it cannot be notarized, and it +carries no team identifier, so nothing can be granted to "shard" as a publisher. None of that +matters for a binary a user builds or installs from a package manager and runs from a terminal: +Gatekeeper judges quarantined downloads, not a process the user execs. + +The signature is per build: the daemon re-signs the shim whenever the embedded bytes change, and a +shim copied from another Mac keeps working, since the seal does not name the machine. + +## What a Developer ID adds + +Signing with a Developer ID certificate (`codesign --sign "Developer ID Application: ..."`) and +notarizing the result lets a downloaded `shard` open without a Gatekeeper refusal, ties the binary to +an Apple team, and lets an MDM allow or deny shard by that team rather than by path or hash. It +changes nothing about the entitlement: `com.apple.security.virtualization` is not restricted, so +an ad-hoc and a Developer ID signature carry it the same way. A future release job can sign the +embedded shim and `shard` itself with a Developer ID; `vzshim.Install` then re-signs the extracted +shim ad-hoc, which is still valid, and a hardened build would sign with the identity instead. + +## What an MDM block looks like + +A managed Mac can forbid virtualization outright. The signal is not shard's: the shim starts, and +the framework refuses the VM with `VZErrorDomain Code=2` (invalid virtual machine configuration) or +the process is denied at `hv_vm_create` with `HV_DENIED`. shard reports that as the shim's exit +with its log tail, for example: + +``` +shard: create: start the vm: Error Domain=VZErrorDomain Code=2 "The virtual machine configuration is invalid." +``` + +The profile behind it is a restrictions payload with `allowVirtualMachines` (or an endpoint security +policy that blocks `com.apple.Virtualization.VirtualMachine`, the framework's helper process). There +is no workaround in shard, and there should not be one: the fix is the MDM policy, and the message +names the framework so the owner knows where to look. diff --git a/docs/provider-vz.md b/docs/provider-vz.md index fe5fcec8a..6c3bd5e75 100644 --- a/docs/provider-vz.md +++ b/docs/provider-vz.md @@ -30,7 +30,7 @@ cross-compile from Linux. `make build-darwin` runs on a Mac with the Command Lin The daemon starts one `shard-vz-shim` process per sandbox, detached, and speaks to it over a unix socket in the sandbox's state directory. The shim holds the VM; the daemon holds the record. A daemon restart re-adopts every running sandbox by that socket (SHARD-235), and only the shim carries the -`com.apple.security.virtualization` entitlement (SHARD-214). +`com.apple.security.virtualization` entitlement (SHARD-214, `docs/macos-signing.md`). This is not only the re-adopt story. **The framework runs at most two VMs in one process.** The third `start` in a process fails with `VZErrorDomain Code=1, the virtual machine failed to start`, diff --git a/pkg/vz/boot_darwin_arm64_test.go b/pkg/vz/boot_darwin_arm64_test.go index 1cff64b10..78f2aea5d 100644 --- a/pkg/vz/boot_darwin_arm64_test.go +++ b/pkg/vz/boot_darwin_arm64_test.go @@ -17,6 +17,8 @@ import ( "syscall" "testing" "time" + + "github.com/presmihaylov/shard/pkg/vzshim" ) // The boot tests want the shard kernel; a Mac without one skips them, and SHARD_KERNEL names one elsewhere. @@ -53,6 +55,9 @@ func prepare(t *testing.T) fixtures { } f := fixtures{kernel: kernel, shim: os.Getenv("SHARD_VZ_SHIM"), initrd: os.Getenv("SHARD_VZ_INITRD")} + if f.shim == "" && vzshim.Embedded() { + f.shim = installShim(t) + } if f.shim == "" { f.shim = buildShim(t) } @@ -63,13 +68,25 @@ func prepare(t *testing.T) fixtures { return f } +// A test binary built after make build-shard-vz-shim carries the shim, the way make build-darwin's daemon does. +func installShim(t *testing.T) string { + t.Helper() + + shim, err := vzshim.Install(t.TempDir()) + if err != nil { + t.Fatalf("vzshim.Install: %v", err) + } + + return shim +} + // The shim needs the virtualization entitlement, and an ad hoc signature is enough to carry it. func buildShim(t *testing.T) string { t.Helper() shim := filepath.Join(t.TempDir(), "shard-vz-shim") run(t, "", "go", "build", "-o", shim, "../../cmd/shard-vz-shim") - run(t, "", "codesign", "--sign", "-", "--force", "--entitlements", "../../cmd/shard-vz-shim/entitlements.plist", shim) + run(t, "", "codesign", "--sign", "-", "--force", "--entitlements", "shim/entitlements.plist", shim) return shim } @@ -460,3 +477,20 @@ func hold() int { fmt.Println(info.PID) select {} } + +// The install itself is proven in pkg/vzshim; this is the boot half of the SHARD-214 AC, over the embedded shim. +func TestTheEmbeddedShimBootsAVM(t *testing.T) { + if !vzshim.Embedded() { + t.Skip("this test binary carries no shim: run make build-shard-vz-shim first") + } + f := prepare(t) + f.shim = installShim(t) + client, info := start(t, f.shim, config(t, f)) + if pid := guestPID(t, client); pid != 1 { + t.Fatalf("the guest answered as pid %d", pid) + } + if _, err := client.Stop(); err != nil { + t.Fatalf("Stop: %v", err) + } + awaitExit(t, info.PID) +} diff --git a/pkg/vzshim/install_darwin_test.go b/pkg/vzshim/install_darwin_test.go new file mode 100644 index 000000000..1d01a5593 --- /dev/null +++ b/pkg/vzshim/install_darwin_test.go @@ -0,0 +1,101 @@ +//go:build darwin + +package vzshim + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "testing" +) + +func embedded(t *testing.T) { + t.Helper() + if !Embedded() { + t.Skip("this test binary carries no shim: run make build-shard-vz-shim first") + } +} + +func TestTheShimInstallsOnceAndCarriesTheEntitlement(t *testing.T) { + embedded(t) + dir := t.TempDir() + + shim, err := Install(dir) + if err != nil { + t.Fatalf("Install: %v", err) + } + first, err := os.Stat(shim) + if err != nil { + t.Fatal(err) + } + if again, err := Install(dir); err != nil || again != shim { + t.Fatalf("second Install: %s, %v", again, err) + } + second, err := os.Stat(shim) + if err != nil { + t.Fatal(err) + } + if !second.ModTime().Equal(first.ModTime()) { + t.Fatal("the second Install rewrote a shim that had not changed") + } + if out := run(t, "codesign", "-d", "--entitlements", "-", shim); !strings.Contains(out, "com.apple.security.virtualization") { + t.Fatalf("the installed shim carries no virtualization entitlement:\n%s", out) + } + if _, err := os.Stat(filepath.Join(dir, entitlementsName)); !os.IsNotExist(err) { + t.Fatalf("the entitlements stayed on disk: %v", err) + } +} + +// Sixteen first-use callers at once is what a daemon that starts sixteen sandboxes does. +func TestConcurrentInstallersLeaveOneSignedShimAndNoDebris(t *testing.T) { + embedded(t) + dir := t.TempDir() + + var wg sync.WaitGroup + errs := make(chan error, 16) + for range 16 { + wg.Go(func() { + if _, err := Install(dir); err != nil { + errs <- err + } + }) + } + wg.Wait() + close(errs) + for err := range errs { + t.Errorf("Install: %v", err) + } + + shim := filepath.Join(dir, shimName) + run(t, "codesign", "--verify", "--strict", shim) + if out := run(t, "codesign", "-d", "--entitlements", "-", shim); !strings.Contains(out, "com.apple.security.virtualization") { + t.Fatalf("the shim carries no virtualization entitlement:\n%s", out) + } + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + var names []string + for _, e := range entries { + names = append(names, e.Name()) + } + if want := []string{shimName, shimName + ".sha256"}; strings.Join(names, " ") != strings.Join(want, " ") { + t.Fatalf("the install left %v, want %v", names, want) + } + if again, err := Install(dir); err != nil || again != shim { + t.Fatalf("Install after the race: %s, %v", again, err) + } +} + +func run(t *testing.T, argv ...string) string { + t.Helper() + + out, err := exec.Command(argv[0], argv[1:]...).CombinedOutput() + if err != nil { + t.Fatalf("%s: %v: %s", strings.Join(argv, " "), err, out) + } + + return string(out) +} diff --git a/pkg/vzshim/shim/README.md b/pkg/vzshim/shim/README.md new file mode 100644 index 000000000..596289f0b --- /dev/null +++ b/pkg/vzshim/shim/README.md @@ -0,0 +1,5 @@ +`make build-shard-vz-shim` puts `shard-vz-shim` here, and `pkg/vzshim` embeds this directory into +the daemon. The binary is ignored by git and `make clean` removes it. `vzshim.Install`, which the +vz provider calls before its first boot (SHARD-218), writes it to the shard root and ad-hoc signs +it there with `entitlements.plist` (`docs/macos-signing.md`). The shim itself imports `pkg/vz` and +never this package, so a rebuild never embeds the previous build. diff --git a/cmd/shard-vz-shim/entitlements.plist b/pkg/vzshim/shim/entitlements.plist similarity index 100% rename from cmd/shard-vz-shim/entitlements.plist rename to pkg/vzshim/shim/entitlements.plist diff --git a/pkg/vzshim/vzshim.go b/pkg/vzshim/vzshim.go new file mode 100644 index 000000000..f7cb77aa8 --- /dev/null +++ b/pkg/vzshim/vzshim.go @@ -0,0 +1,109 @@ +// Package vzshim carries shard-vz-shim inside the daemon, apart from pkg/vz so the shim never embeds its own previous build. +package vzshim + +import ( + "bytes" + "crypto/sha256" + "embed" + "errors" + "fmt" + "io/fs" + "os" + "os/exec" + "path/filepath" +) + +// The shim binary is built into this directory by make, so a build without it still compiles and says so at run time. +// +//go:embed shim +var shimFS embed.FS + +const ( + shimName = "shard-vz-shim" + entitlementsName = "entitlements.plist" +) + +// ErrNoShim is what Install returns from a binary built by go build alone, without make build-darwin. +var ErrNoShim = errors.New("vzshim: this binary was built without shard-vz-shim: run make build-darwin") + +// Embedded reports whether this binary carries the shim, so a test can skip instead of fail without it. +func Embedded() bool { + _, err := fs.Stat(shimFS, "shim/"+shimName) + + return err == nil +} + +// Install writes the embedded shim under dir, ad-hoc signed with the virtualization entitlement, once per build; concurrent callers each rename their own signed copy in. +func Install(dir string) (string, error) { + body, err := fs.ReadFile(shimFS, "shim/"+shimName) + if err != nil { + return "", ErrNoShim + } + path := filepath.Join(dir, shimName) + // codesign rewrites the bytes, so a stamp of the embedded build says whether the installed shim is this one. + stamp := filepath.Join(dir, shimName+".sha256") + sum := fmt.Appendf(nil, "%x\n", sha256.Sum256(body)) + if current, err := os.ReadFile(stamp); err == nil && bytes.Equal(current, sum) { + if _, err := os.Stat(path); err == nil { + return path, nil + } + } + + entitlements, err := fs.ReadFile(shimFS, "shim/"+entitlementsName) + if err != nil { + return "", fmt.Errorf("read the embedded entitlements: %w", err) + } + plist, err := writeTemp(dir, entitlementsName, entitlements, 0o600) + if err != nil { + return "", err + } + defer os.Remove(plist) + tmp, err := writeTemp(dir, shimName, body, 0o700) + if err != nil { + return "", err + } + if err := sign(tmp, plist); err != nil { + return "", errors.Join(err, os.Remove(tmp)) + } + // A running shim keeps its old inode when the file is replaced by rename, never truncated under it. + if err := os.Rename(tmp, path); err != nil { + return "", errors.Join(fmt.Errorf("install the shim: %w", err), os.Remove(tmp)) + } + stamped, err := writeTemp(dir, stamp, sum, 0o600) + if err != nil { + return "", err + } + if err := os.Rename(stamped, stamp); err != nil { + return "", errors.Join(fmt.Errorf("stamp the shim: %w", err), os.Remove(stamped)) + } + + return path, nil +} + +// writeTemp puts body in a file of its own under dir, so two installers never write into one path. +func writeTemp(dir, name string, body []byte, mode os.FileMode) (string, error) { + f, err := os.CreateTemp(dir, filepath.Base(name)+".*.tmp") + if err != nil { + return "", fmt.Errorf("create %s: %w", name, err) + } + _, err = f.Write(body) + if err == nil { + err = f.Chmod(mode) + } + if err := errors.Join(err, f.Close()); err != nil { + return "", errors.Join(fmt.Errorf("write %s: %w", name, err), os.Remove(f.Name())) + } + + return f.Name(), nil +} + +// An ad-hoc signature is enough to carry the entitlement on this Mac; docs/macos-signing.md says what it cannot do. +func sign(path, plist string) error { + cmd := exec.Command("codesign", "--sign", "-", "--force", "--entitlements", plist, path) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("codesign the shim: %w: %s", err, bytes.TrimSpace(out)) + } + + return nil +} diff --git a/pkg/vzshim/vzshim_test.go b/pkg/vzshim/vzshim_test.go new file mode 100644 index 000000000..ac82e57db --- /dev/null +++ b/pkg/vzshim/vzshim_test.go @@ -0,0 +1,46 @@ +package vzshim + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestABinaryWithoutTheShimSaysSoInsteadOfInstallingNothing(t *testing.T) { + if Embedded() { + t.Skip("this test binary carries the shim") + } + if _, err := Install(t.TempDir()); !errors.Is(err, ErrNoShim) { + t.Fatalf("Install without a shim: %v", err) + } +} + +// The daemon must reference this package or the linker drops the embed; the shim must not, or it embeds its previous build. +func TestTheDaemonLinksTheEmbeddedShimAndTheShimDoesNot(t *testing.T) { + daemon := filepath.Join(t.TempDir(), "shard") + build := exec.Command("go", "build", "-o", daemon, "../../cmd/shard") + build.Env = append(os.Environ(), "GOOS=darwin", "GOARCH=arm64", "CGO_ENABLED=0") + if out, err := build.CombinedOutput(); err != nil { + t.Fatalf("go build: %v: %s", err, out) + } + symbols, err := exec.Command("go", "tool", "nm", daemon).Output() + if err != nil { + t.Fatalf("go tool nm: %v", err) + } + if !strings.Contains(string(symbols), "pkg/vzshim.shimFS") { + t.Fatal("the darwin daemon carries no embedded shim: the linker dropped pkg/vzshim") + } + + list := exec.Command("go", "list", "-deps", "../../cmd/shard-vz-shim") + list.Env = append(os.Environ(), "GOOS=darwin", "CGO_ENABLED=0") + out, err := list.CombinedOutput() + if err != nil { + t.Fatalf("go list: %v: %s", err, out) + } + if strings.Contains(string(out), "github.com/presmihaylov/shard/pkg/vzshim\n") { + t.Fatal("cmd/shard-vz-shim links pkg/vzshim, so it would embed its own previous build") + } +}