9722a39639
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
135 lines
4.6 KiB
Go
135 lines
4.6 KiB
Go
package launcher
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
type runtimeBundle struct {
|
|
Dir string
|
|
Launcher string
|
|
Source string
|
|
}
|
|
|
|
type agentctlRemoteHelper struct {
|
|
Name string
|
|
Label string
|
|
// MustBeSigned requires the Mach-O helper to carry an ad-hoc (or real) code
|
|
// signature. Apple Silicon (AMFI) SIGKILLs unsigned darwin/arm64 binaries on
|
|
// launch, so an unsigned arm64 helper would fail on every Mac it's uploaded
|
|
// to — catch that at bundle-validation time, not at remote-launch time.
|
|
// darwin/amd64 is intentionally exempt: Intel macOS does not enforce code
|
|
// signing at exec, and Go's linker does not ad-hoc-sign amd64 cross-builds.
|
|
MustBeSigned bool
|
|
}
|
|
|
|
var requiredAgentctlRemoteHelpers = []agentctlRemoteHelper{
|
|
{Name: "agentctl-linux-amd64", Label: "agentctl linux/amd64 helper"},
|
|
{Name: "agentctl-linux-arm64", Label: "agentctl linux/arm64 helper"},
|
|
{Name: "agentctl-darwin-arm64", Label: "agentctl darwin/arm64 helper", MustBeSigned: true},
|
|
{Name: "agentctl-darwin-amd64", Label: "agentctl darwin/amd64 helper"},
|
|
}
|
|
|
|
func resolveRuntimeBundle() (runtimeBundle, error) {
|
|
dir := os.Getenv("KANDEV_BUNDLE_DIR")
|
|
if dir == "" {
|
|
return runtimeBundle{}, fmt.Errorf("no Kandev runtime found; KANDEV_BUNDLE_DIR is not set")
|
|
}
|
|
return validateRuntimeBundle(dir, "env")
|
|
}
|
|
|
|
func validateRuntimeBundle(dir, source string) (runtimeBundle, error) {
|
|
launcher := filepath.Join(dir, "bin", executableName("kandev"))
|
|
if !exists(launcher) {
|
|
return runtimeBundle{}, fmt.Errorf("launcher binary not found in bundle at %s", launcher)
|
|
}
|
|
agentctl := filepath.Join(dir, "bin", executableName("agentctl"))
|
|
if !exists(agentctl) {
|
|
return runtimeBundle{}, fmt.Errorf("agentctl binary not found in bundle at %s", agentctl)
|
|
}
|
|
for _, helper := range requiredAgentctlRemoteHelpers {
|
|
path := filepath.Join(dir, "bin", helper.Name)
|
|
if !exists(path) {
|
|
return runtimeBundle{}, fmt.Errorf("%s not found in bundle at %s", helper.Label, path)
|
|
}
|
|
if helper.MustBeSigned {
|
|
signed, ok := machoHasCodeSignature(path)
|
|
if !ok {
|
|
// Fail closed: a helper we can't parse as a thin arm64 Mach-O
|
|
// (read error, truncation, unexpected/fat layout) can't be
|
|
// verified as signed, and an unverifiable darwin/arm64 helper
|
|
// would still be SIGKILLed by Apple Silicon at launch.
|
|
return runtimeBundle{}, fmt.Errorf(
|
|
"%s at %s is not a parsable thin darwin/arm64 Mach-O; cannot verify its "+
|
|
"code signature (rebuild it via 'make -C apps/backend build-agentctl-remote')",
|
|
helper.Label, path)
|
|
}
|
|
if !signed {
|
|
return runtimeBundle{}, fmt.Errorf(
|
|
"%s at %s is not code-signed; Apple Silicon will refuse to run it "+
|
|
"(build it via 'make -C apps/backend build-agentctl-remote', which ad-hoc-signs darwin helpers)",
|
|
helper.Label, path)
|
|
}
|
|
}
|
|
}
|
|
return runtimeBundle{Dir: dir, Launcher: launcher, Source: source}, nil
|
|
}
|
|
|
|
// machoHasCodeSignature reports whether a thin 64-bit Mach-O carries an
|
|
// LC_CODE_SIGNATURE load command. The second return is false when the file
|
|
// can't be parsed as a thin Mach-O (e.g. unexpected format) so callers can
|
|
// treat "undeterminable" as non-fatal rather than failing on a parse quirk.
|
|
func machoHasCodeSignature(path string) (signed bool, ok bool) {
|
|
const (
|
|
machoMagic64LE = 0xfeedfacf // MH_MAGIC_64, written little-endian on disk
|
|
lcCodeSignature = 0x1d // LC_CODE_SIGNATURE
|
|
machoHeaderSize64 = 32
|
|
loadCommandHdrSize = 8
|
|
)
|
|
const maxScanBytes = 64 * 1024 // more than enough for any real Mach-O load-command table
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return false, false
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
data := make([]byte, maxScanBytes)
|
|
n, err := io.ReadFull(f, data)
|
|
if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
|
|
return false, false
|
|
}
|
|
data = data[:n]
|
|
if len(data) < machoHeaderSize64 {
|
|
return false, false
|
|
}
|
|
if binary.LittleEndian.Uint32(data[0:4]) != machoMagic64LE {
|
|
return false, false // not a thin LE 64-bit Mach-O (fat/other) — don't judge
|
|
}
|
|
ncmds := binary.LittleEndian.Uint32(data[16:20])
|
|
off := machoHeaderSize64
|
|
for i := uint32(0); i < ncmds; i++ {
|
|
if off+loadCommandHdrSize > len(data) {
|
|
return false, false // truncated load-command table
|
|
}
|
|
cmd := binary.LittleEndian.Uint32(data[off : off+4])
|
|
cmdSize := binary.LittleEndian.Uint32(data[off+4 : off+8])
|
|
if cmd == lcCodeSignature {
|
|
return true, true
|
|
}
|
|
if cmdSize < loadCommandHdrSize {
|
|
return false, false // malformed
|
|
}
|
|
off += int(cmdSize)
|
|
}
|
|
return false, true
|
|
}
|
|
|
|
func executableName(name string) string {
|
|
if os.PathSeparator == '\\' {
|
|
return name + ".exe"
|
|
}
|
|
return name
|
|
}
|