From 2102e2c19f1b860f8b057bfc651149d0424cb07a Mon Sep 17 00:00:00 2001 From: Majd Alfhaily Date: Sat, 10 Oct 2026 18:31:00 +0200 Subject: [PATCH 1/4] chore: update go toolchain and dependencies --- .github/workflows/dry-build.yml | 8 +- .github/workflows/integration-tests.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 8 +- .github/workflows/unit-tests.yml | 2 +- go.mod | 60 ++++--- go.sum | 200 +++++++++++++++++------- internal/lzfsecompat/go.mod | 5 + internal/lzfsecompat/lzfse.go | 37 +++++ 9 files changed, 238 insertions(+), 86 deletions(-) create mode 100644 internal/lzfsecompat/go.mod create mode 100644 internal/lzfsecompat/lzfse.go diff --git a/.github/workflows/dry-build.yml b/.github/workflows/dry-build.yml index 9e9883d4..25461b83 100644 --- a/.github/workflows/dry-build.yml +++ b/.github/workflows/dry-build.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.25.0" + go-version: "1.26.5" cache: true - run: go build -o ipatool-$GOOS-$GOARCH.exe env: @@ -38,7 +38,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.25.0" + go-version: "1.26.5" cache: true - run: go build -o ipatool-linux-$GOARCH env: @@ -54,7 +54,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.25.0" + go-version: "1.26.5" cache: true - run: go build -o ipatool-$GOOS-$GOARCH env: @@ -70,7 +70,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.25.0" + go-version: "1.26.5" cache: true - run: brew install ldid cmake - run: ./tools/build-ios.sh diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 96e3a0a9..0dd5155a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.25.0" + go-version: "1.26.5" cache: true - run: go build -o ipatool env: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 377c8965..6a15bfe2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.25.0" + go-version: "1.26.5" cache: true - run: go generate github.com/majd/ipatool/... - uses: golangci/golangci-lint-action@v8 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b79c15a..626ae391 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.25.0" + go-version: "1.26.5" cache: true - run: go generate github.com/majd/ipatool/... - run: go test -v github.com/majd/ipatool/... @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.25.0" + go-version: "1.26.5" cache: true - run: go build -ldflags="-X github.com/majd/ipatool/v2/cmd.version=$VERSION" -o ipatool-$VERSION-windows-arm64.exe env: @@ -97,7 +97,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.25.0" + go-version: "1.26.5" cache: true - run: go build -ldflags="-X github.com/majd/ipatool/v2/cmd.version=$VERSION" -o ipatool-$VERSION-linux-$GOARCH env: @@ -116,7 +116,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.25.0" + go-version: "1.26.5" cache: true - run: brew install ldid cmake - run: ./tools/build-ios.sh "ipatool-$VERSION-ios-arm64" diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index a62f6953..e80e9cdf 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.25.0" + go-version: "1.26.5" cache: true - run: go generate github.com/majd/ipatool/... - run: go test -v github.com/majd/ipatool/... diff --git a/go.mod b/go.mod index 63955e14..0585cd4c 100644 --- a/go.mod +++ b/go.mod @@ -1,43 +1,56 @@ module github.com/majd/ipatool/v2 -go 1.25.0 +go 1.26.5 + +// go-apfs requires this package path; keep decoding pure Go for cross-platform builds. +replace github.com/blacktop/lzfse-cgo => ./internal/lzfsecompat require ( github.com/avast/retry-go v3.0.0+incompatible + github.com/blacktop/go-apfs v1.0.28 github.com/blacktop/go-macho v1.1.282 github.com/bodgit/sevenzip v1.6.3 github.com/byteness/go-keychain v0.0.0-20191008050251-8e49817e8af4 github.com/byteness/keyring v1.9.0 + github.com/deploymenttheory/go-macos-pkg v0.3.0 github.com/ebitengine/purego v0.10.2 + github.com/go-compressions/lzfse v0.3.0 github.com/juju/persistent-cookiejar v1.0.0 github.com/klauspost/compress v1.18.6 github.com/modelcontextprotocol/go-sdk v1.8.0 github.com/onsi/ginkgo/v2 v2.5.0 github.com/onsi/gomega v1.24.0 github.com/rs/zerolog v1.28.0 - github.com/schollz/progressbar/v3 v3.13.1 - github.com/spf13/cobra v1.6.1 + github.com/schollz/progressbar/v3 v3.19.1 + github.com/spf13/cobra v1.10.2 github.com/thediveo/enumflag/v2 v2.0.1 go.uber.org/mock v0.4.0 - golang.org/x/sys v0.45.0 - golang.org/x/term v0.43.0 - howett.net/plist v1.0.0 - howett.net/ranger v0.0.0-20171016084633-e2e137620847 + golang.org/x/sys v0.48.0 + golang.org/x/term v0.46.0 + howett.net/plist v1.0.1 ) require ( github.com/1Password/connect-sdk-go v1.5.4-0.20250417152128-c154b387248b // indirect github.com/1password/onepassword-sdk-go v0.4.1-beta.1 // indirect + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/andybalholm/brotli v1.2.1 // indirect + github.com/apex/log v1.9.0 // indirect github.com/blacktop/go-dwarf v1.0.14 // indirect + github.com/blacktop/go-plist v1.0.2 // indirect + github.com/blacktop/lzfse-cgo v1.2.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/byteness/go-libsecret v0.0.0-20260108215642-107379d3dee0 // indirect github.com/byteness/percent v0.2.2 // indirect github.com/danieljoos/wincred v1.2.3 // indirect + github.com/dustin/go-humanize v1.1.0 // indirect github.com/dvsekhvalnov/jose2go v1.8.0 // indirect github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 // indirect github.com/extism/go-sdk v1.7.1 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/frankban/quicktest v1.14.6 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect @@ -45,41 +58,46 @@ require ( github.com/google/jsonschema-go v0.4.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/noamcohen97/touchid-go v0.3.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pierrec/lz4/v4 v4.1.26 // indirect + github.com/pierrec/lz4/v4 v4.1.29 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/encoding v0.5.4 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/stangelandcl/ppmd v0.1.0 // indirect github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect github.com/tetratelabs/wazero v1.11.0 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect - github.com/ulikunitz/xz v0.5.15 // indirect + github.com/ulikunitz/xz v0.5.17 // indirect + github.com/vbauerster/mpb/v7 v7.5.3 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/atomic v1.11.0 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect - golang.org/x/crypto v0.52.0 // indirect + golang.org/x/crypto v0.57.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.55.0 // indirect + golang.org/x/mod v0.41.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/text v0.42.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools v0.49.0 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/retry.v1 v1.0.3 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 3e3c7d73..468eaf75 100644 --- a/go.sum +++ b/go.sum @@ -4,14 +4,29 @@ github.com/1password/onepassword-sdk-go v0.4.1-beta.1 h1:b/tt+bAXFXR5f3/irmPAayt github.com/1password/onepassword-sdk-go v0.4.1-beta.1/go.mod h1:j/CbzhucTywjlYrd6SE6k0LcQaFZ2l8OLBsAsOYtvD0= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= +github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= +github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= +github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= +github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= +github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= +github.com/blacktop/go-apfs v1.0.28 h1:kXihmwrGdKZjUn37ULxWHG0R6IgtQKOqmtRHo6QuAa8= +github.com/blacktop/go-apfs v1.0.28/go.mod h1:i2Sfy/il/t2ADGIGPN0FAm+7CAot7TnQwgmMQXzF0xs= github.com/blacktop/go-dwarf v1.0.14 h1:OjmzfSgg/qAKckn2tWFebcgKgJ7HOqCj7bS+CiE1lrY= github.com/blacktop/go-dwarf v1.0.14/go.mod h1:4W2FKgSFYcZLDwnR7k+apv5i3nrau4NGl9N6VQ9DSTo= github.com/blacktop/go-macho v1.1.282 h1:DW3HYz5zVCT6+Jlp1+hxauYvEgxqZsGMvu8/1j4+Ibg= github.com/blacktop/go-macho v1.1.282/go.mod h1:Hc5E2Lvt/U1VT+jOxr1O5l/LNFJeMYK4eAmDfazTiGc= +github.com/blacktop/go-plist v1.0.2 h1:DMX8uBiE308HWZkO9o37j7Z2b0neI3GSzN2caNa2zXk= +github.com/blacktop/go-plist v1.0.2/go.mod h1:fqVhCVVXVJWsIGY9QPgdK0mDWZD82HrRMfU5PanvdTA= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= github.com/bodgit/sevenzip v1.6.3 h1:t7K1jcgPnXdZyRHr68vq5L9vO/WmjDJEiGUwTR6i6gk= @@ -26,13 +41,21 @@ github.com/byteness/keyring v1.9.0 h1:9m+7pru+JkMJ8wxma5id5m6pSt2Zx/xPj77kyM+yAV github.com/byteness/keyring v1.9.0/go.mod h1:a6CV8t/qfEUERPTP+dqeVfKgib2wz3PIYbMWRfmvmes= github.com/byteness/percent v0.2.2 h1:vnIFh8WBR1xoC+U2etz0EMB1cgp+vsK6vynqTCeDziU= github.com/byteness/percent v0.2.2/go.mod h1:nwavge92FhIyfnldz4YWZD8uxPVvdh8NlzLRd1VYRDs= +github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= +github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deploymenttheory/go-macos-pkg v0.3.0 h1:1pbCbFpib7lzAdu0FUnCW5frhYtjEmqO1iNg0kIKthI= +github.com/deploymenttheory/go-macos-pkg v0.3.0/go.mod h1:aSMBLo4teBpMNx6bioyGfF22Kkdu1dodyWiyn0PLFjg= +github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg= +github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI= github.com/dvsekhvalnov/jose2go v1.8.0 h1:LqkkVKAlHFfH9LOEl5fe4p/zL02OhWE7pCufMBG2jLA= github.com/dvsekhvalnov/jose2go v1.8.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 h1:idfl8M8rPW93NehFw5H1qqH8yG158t5POr+LX9avbJY= @@ -41,8 +64,16 @@ github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7 github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= -github.com/frankban/quicktest v1.2.2 h1:xfmOhhoH5fGPgbEAlhLpJH9p0z/0Qizio9osmvn9IUY= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-compressions/lzfse v0.3.0 h1:slxIjY9HeG2llJSoXTwTnQ+u/iKMbr2WHUwaNAl/2yI= +github.com/go-compressions/lzfse v0.3.0/go.mod h1:geg9IRFbMNHPgbm8xNwF3AnYTsldTX9YptJ0DOWzYv8= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -52,85 +83,115 @@ github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f h1:Fnl4pzx8SR7k7JuzyW8lEtSFH6EQ8xgcypgIn8pcGIE= github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a h1:45JtCyuNYE+QN9aPuR1ID9++BQU+NMTMudHSuaK0Las= github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a/go.mod h1:RVHtZuvrpETIepiNUrNlih2OynoFf1eM6DGC6dloXzk= github.com/juju/persistent-cookiejar v1.0.0 h1:Ag7+QLzqC2m+OYXy2QQnRjb3gTkEBSZagZ6QozwT3EQ= github.com/juju/persistent-cookiejar v1.0.0/go.mod h1:zrbmo4nBKaiP/Ez3F67ewkMbzGYfXyMvRtbOfuAwG0w= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/modelcontextprotocol/go-sdk v1.8.0 h1:KIvahhYqwtbeniWVPs3TcXEA7b8jEtwfBpOTAI+Urx4= github.com/modelcontextprotocol/go-sdk v1.8.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/noamcohen97/touchid-go v0.3.0 h1:fcXxVCizysD7KHRR6hrURt3nyNIs5JBGSbOIidD/3wo= github.com/noamcohen97/touchid-go v0.3.0/go.mod h1:X9MRNIBGEmPqwpDm1G3fQOAQX7fwBlhzUbnkDTxuta0= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo/v2 v2.5.0 h1:TRtrvv2vdQqzkwrQ1ke6vtXf7IK34RBUJafIy1wMwls= github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.24.0 h1:+0glovB9Jd6z3VR+ScSwQqXVTIfJcGA9UBM8yzQxhqg= github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= -github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pierrec/lz4/v4 v4.1.29 h1:CDQY6qZOLI4DW0Nx6R1vRrifrCeQHnNXkMb0hZWXFjg= +github.com/pierrec/lz4/v4 v4.1.29/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a h1:3QH7VyOaaiUHNrA9Se4YQIRkDTCw1EJls9xTUCaCeRM= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= +github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE= -github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ= +github.com/schollz/progressbar/v3 v3.19.1 h1:iv8BgwOvdML/S3p84uBpy/IMigv4U9594vPZYa2EdrU= +github.com/schollz/progressbar/v3 v3.19.1/go.mod h1:LFL7jqimKxfhero4K1eCkUr/6R39AgQeiPCJtlTWIW8= github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= +github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stangelandcl/ppmd v0.1.0 h1:0gOKtSdWyXxpRW55H/dZ3AgaJqjrtlIrAvsokFwp7ug= github.com/stangelandcl/ppmd v0.1.0/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -139,6 +200,8 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -150,12 +213,23 @@ github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbw github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= github.com/thediveo/enumflag/v2 v2.0.1 h1:2bmWZPD2uSARDsOjXIdLRlNcYBFNF9xX0RNUNF2vKic= github.com/thediveo/enumflag/v2 v2.0.1/go.mod h1:SyxyCNvv0QeRtZ7fjuaUz4FRLC3cWuDiD7QdORU0MGg= +github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= +github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= +github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= +github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= +github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= +github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= +github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= -github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.17 h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4= +github.com/ulikunitz/xz v0.5.17/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw= +github.com/vbauerster/mpb/v7 v7.5.3 h1:BkGfmb6nMrrBQDFECR/Q7RkKCw7ylMetCb4079CGs4w= +github.com/vbauerster/mpb/v7 v7.5.3/go.mod h1:i+h4QY6lmLvBNK2ah1fSreiw3ajskRlBp9AhY/PnuOE= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= @@ -166,49 +240,67 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs= gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= -howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -howett.net/ranger v0.0.0-20171016084633-e2e137620847 h1:jHX+2Sv8rQNb1nG2G9pcQLdVoWXBLW5fcd2gnfHluCU= -howett.net/ranger v0.0.0-20171016084633-e2e137620847/go.mod h1:ZWGIG4mR6Ck+CdmFqK2/jox5vO2OAS8Qpb33HB8z0og= +howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= +howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= diff --git a/internal/lzfsecompat/go.mod b/internal/lzfsecompat/go.mod new file mode 100644 index 00000000..57dacd83 --- /dev/null +++ b/internal/lzfsecompat/go.mod @@ -0,0 +1,5 @@ +module github.com/blacktop/lzfse-cgo + +go 1.20 + +require github.com/go-compressions/lzfse v0.3.0 diff --git a/internal/lzfsecompat/lzfse.go b/internal/lzfsecompat/lzfse.go new file mode 100644 index 00000000..3149dbe6 --- /dev/null +++ b/internal/lzfsecompat/lzfse.go @@ -0,0 +1,37 @@ +// Package lzfse provides the decoder API used by go-apfs without requiring CGo. +package lzfse + +import pure "github.com/go-compressions/lzfse" + +// DecodeBuffer decompresses an LZFSE stream. Invalid streams return nil. +func DecodeBuffer(encoded []byte) []byte { + decoded, err := pure.Decompress(encoded) + if err != nil { + return nil + } + + return decoded +} + +// DecodeBufferInto decompresses an LZFSE stream into decoded and returns the +// number of bytes written. A zero result indicates failure or insufficient +// output space. +func DecodeBufferInto(encoded, decoded []byte) int { + data, err := pure.Decompress(encoded) + if err != nil || len(data) > len(decoded) { + return 0 + } + + return copy(decoded, data) +} + +// DecodeLZVNBuffer decompresses a raw LZVN stream into decoded and returns the +// number of bytes written. A zero result indicates failure. +func DecodeLZVNBuffer(encoded, decoded []byte) uint { + data, err := pure.DecompressLZVN(encoded, len(decoded)) + if err != nil { + return 0 + } + + return uint(copy(decoded, data)) +} From 0d7c4c640a6ec319bdbcc7399ee9450cbb9cf186 Mon Sep 17 00:00:00 2001 From: Majd Alfhaily Date: Sat, 10 Oct 2026 18:31:07 +0200 Subject: [PATCH 2/4] feat: resolve macos 27 assets natively --- internal/sap/assets/aea.go | 634 +++++++++++++ internal/sap/assets/apfs.go | 864 ++++++++++++++++++ internal/sap/assets/assets.go | 199 +--- internal/sap/assets/dyld.go | 685 ++++++++++++++ internal/sap/assets/http_range.go | 163 ++++ internal/sap/assets/remote.go | 170 ++++ .../sap/assets/remote_integration_test.go | 68 ++ internal/sap/assets/remote_test.go | 58 ++ internal/sap/assets/storeagent.go | 86 -- internal/sap/assets/storeagent_test.go | 34 +- internal/sap/cpio/reader.go | 86 -- internal/sap/cpio/reader_test.go | 52 -- 12 files changed, 2695 insertions(+), 404 deletions(-) create mode 100644 internal/sap/assets/aea.go create mode 100644 internal/sap/assets/apfs.go create mode 100644 internal/sap/assets/dyld.go create mode 100644 internal/sap/assets/http_range.go create mode 100644 internal/sap/assets/remote.go create mode 100644 internal/sap/assets/remote_integration_test.go create mode 100644 internal/sap/assets/remote_test.go delete mode 100644 internal/sap/assets/storeagent.go delete mode 100644 internal/sap/cpio/reader.go delete mode 100644 internal/sap/cpio/reader_test.go diff --git a/internal/sap/assets/aea.go b/internal/sap/assets/aea.go new file mode 100644 index 00000000..70a04267 --- /dev/null +++ b/internal/sap/assets/aea.go @@ -0,0 +1,634 @@ +//nolint:wsl // Binary format parsing is clearer when related bounds checks remain adjacent. +package assets + +import ( + "bytes" + "context" + "crypto/aes" + "crypto/cipher" + "crypto/ecdh" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/hkdf" + "crypto/hmac" + "crypto/hpke" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/binary" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "sync" + + "github.com/go-compressions/lzfse" +) + +const ( + aeaMagic = "AEA1" + aeaSymmetricProfile = 1 + aeaMainKeyInfo = "AEA_AMK" + aeaRootKeyInfo = "AEA_RHEK" + aeaClusterKeyInfo = "AEA_CK" + aeaClusterHeaderKeyInfo = "AEA_CHEK" + aeaSegmentKeyInfo = "AEA_SK" + aeaLZFSECompression = 'e' + aeaSHA256Checksum = 2 + aeaDecodedSegmentCacheCount = 64 + maxAEAAuthDataSize = 1 << 20 + maxFCSKeySize = 64 << 10 +) + +type aeaHeader struct { + Magic [4]byte + ProfileAndScryptStrength uint32 + AuthDataLength uint32 +} + +type aeaMAC [sha256.Size]byte + +type encryptedAEARootHeader struct { + MAC aeaMAC + Data [48]byte + ClusterMAC aeaMAC +} + +type aeaRootHeader struct { + FileSize uint64 + EncryptedSize uint64 + SegmentSize uint32 + SegmentsPerCluster uint32 + Compression uint8 + Checksum uint8 + _ [22]byte +} + +type aeaHeaderKey struct { + MAC aeaMAC + Key [32]byte + IV [aes.BlockSize]byte +} + +type aeaSegmentHeader struct { + DecompressedSize uint32 + CompressedSize uint32 + Checksum [sha256.Size]byte +} + +type aeaSegment struct { + inputOffset int64 + outputOffset int64 + compressedSize uint32 + decompressedSize uint32 + checksum [sha256.Size]byte + mac aeaMAC + cluster uint32 + index uint32 +} + +type aeaReader struct { + source io.ReaderAt + size int64 + segmentSize uint32 + mainKey []byte + segments []aeaSegment + cache map[int][]byte + cacheOrder []int + cacheMu sync.Mutex +} + +type fcsResponse struct { + EncRequest string `json:"enc-request"` + WrappedKey string `json:"wrapped-key"` +} + +func newAEAReader(ctx context.Context, client *http.Client, source io.ReaderAt, sourceSize int64) (*aeaReader, error) { + var header aeaHeader + if err := readBinaryAt(source, 0, &header); err != nil { + return nil, fmt.Errorf("read AEA header: %w", err) + } + + if string(header.Magic[:]) != aeaMagic { + return nil, fmt.Errorf("invalid AEA magic %q", header.Magic) + } + + if header.ProfileAndScryptStrength&0xffffff != aeaSymmetricProfile { + return nil, fmt.Errorf("unsupported AEA profile %d", header.ProfileAndScryptStrength&0xffffff) + } + + if header.AuthDataLength > maxAEAAuthDataSize { + return nil, fmt.Errorf("AEA authentication data is too large: %d", header.AuthDataLength) + } + + headerSize := int64(binary.Size(header)) + authData := make([]byte, header.AuthDataLength) + if _, err := source.ReadAt(authData, headerSize); err != nil { + return nil, fmt.Errorf("read AEA authentication data: %w", err) + } + + metadata, err := parseAEAMetadata(authData) + if err != nil { + return nil, err + } + + symmetricKey, err := resolveAEASymmetricKey(ctx, client, metadata) + if err != nil { + return nil, err + } + + mainSaltOffset := headerSize + int64(len(authData)) + mainSalt := make([]byte, sha256.Size) + if _, err := source.ReadAt(mainSalt, mainSaltOffset); err != nil { + return nil, fmt.Errorf("read AEA main salt: %w", err) + } + + mainKey, err := deriveAEAKey(symmetricKey, mainSalt, binary.LittleEndian.AppendUint32([]byte(aeaMainKeyInfo), header.ProfileAndScryptStrength), len(symmetricKey)) + if err != nil { + return nil, fmt.Errorf("derive AEA main key: %w", err) + } + + rootOffset := mainSaltOffset + int64(len(mainSalt)) + + var encryptedRoot encryptedAEARootHeader + if err := readBinaryAt(source, rootOffset, &encryptedRoot); err != nil { + return nil, fmt.Errorf("read encrypted AEA root header: %w", err) + } + + rootKey, err := deriveAEAHeaderKey(mainKey, []byte(aeaRootKeyInfo)) + if err != nil { + return nil, fmt.Errorf("derive AEA root key: %w", err) + } + + rootSalt := make([]byte, 0, len(encryptedRoot.ClusterMAC)+len(authData)) + rootSalt = append(rootSalt, encryptedRoot.ClusterMAC[:]...) + rootSalt = append(rootSalt, authData...) + computedRootMAC := calculateAEAMAC(rootKey.MAC[:], encryptedRoot.Data[:], rootSalt) + if !hmac.Equal(encryptedRoot.MAC[:], computedRootMAC[:]) { + return nil, errors.New("AEA root header failed authentication") + } + + rootData, err := decryptAEACTR(encryptedRoot.Data[:], rootKey.Key[:], rootKey.IV[:]) + if err != nil { + return nil, fmt.Errorf("decrypt AEA root header: %w", err) + } + + var root aeaRootHeader + if err := binary.Read(bytes.NewReader(rootData), binary.LittleEndian, &root); err != nil { + return nil, fmt.Errorf("decode AEA root header: %w", err) + } + + if root.FileSize == 0 || root.FileSize > ^uint64(0)>>1 { + return nil, fmt.Errorf("invalid AEA output size %d", root.FileSize) + } + + if root.EncryptedSize != uint64(sourceSize) { + return nil, fmt.Errorf("AEA encrypted size is %d, expected %d", root.EncryptedSize, sourceSize) + } + + if root.SegmentSize == 0 || root.SegmentsPerCluster == 0 { + return nil, errors.New("AEA has an invalid segment layout") + } + + if root.Compression != aeaLZFSECompression || root.Checksum != aeaSHA256Checksum { + return nil, fmt.Errorf("unsupported AEA encoding: compression %q, checksum %d", root.Compression, root.Checksum) + } + + reader := &aeaReader{ + source: source, + size: int64(root.FileSize), + segmentSize: root.SegmentSize, + mainKey: mainKey, + segments: make([]aeaSegment, (root.FileSize+uint64(root.SegmentSize)-1)/uint64(root.SegmentSize)), + cache: make(map[int][]byte, aeaDecodedSegmentCacheCount), + } + + clusterOffset := rootOffset + int64(binary.Size(encryptedRoot)) + if err := reader.indexClusters(clusterOffset, encryptedRoot.ClusterMAC, root, sourceSize); err != nil { + return nil, err + } + + return reader, nil +} + +func parseAEAMetadata(data []byte) (map[string][]byte, error) { + metadata := make(map[string][]byte) + reader := bytes.NewReader(data) + + for reader.Len() != 0 { + var length uint32 + if err := binary.Read(reader, binary.LittleEndian, &length); err != nil { + return nil, fmt.Errorf("read AEA metadata length: %w", err) + } + + if length < 5 || uint64(length-4) > uint64(reader.Len()) { + return nil, fmt.Errorf("invalid AEA metadata record length %d", length) + } + + record := make([]byte, length-4) + if _, err := io.ReadFull(reader, record); err != nil { + return nil, fmt.Errorf("read AEA metadata record: %w", err) + } + + key, value, ok := bytes.Cut(record, []byte{0}) + if !ok || len(key) == 0 { + return nil, errors.New("invalid AEA metadata record") + } + + metadata[string(key)] = value + } + + return metadata, nil +} + +// resolveAEASymmetricKey downloads the Apple-published FCS P-256 private key +// identified by the AEA metadata. That key is the HPKE recipient key for this +// image's wrapped symmetric key; the unwrapped key is then used to derive the +// AES and HMAC keys that decrypt and authenticate the AEA headers and segments. +// It is restore-image key material, not a user credential. +func resolveAEASymmetricKey(ctx context.Context, client *http.Client, metadata map[string][]byte) ([]byte, error) { + keyURLData, ok := metadata["com.apple.wkms.fcs-key-url"] + if !ok { + return nil, errors.New("AEA metadata does not contain an FCS key URL") + } + + keyURL, err := url.Parse(string(keyURLData)) + if err != nil || keyURL.Scheme != "https" || keyURL.Hostname() != "wkms-public.apple.com" { + return nil, fmt.Errorf("invalid Apple FCS key URL %q", keyURLData) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, keyURL.String(), nil) + if err != nil { + return nil, fmt.Errorf("create Apple FCS key request: %w", err) + } + + response, err := client.Do(request) + if err != nil { + return nil, fmt.Errorf("download Apple FCS key: %w", err) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("download Apple FCS key: server returned %s", response.Status) + } + + pemData, err := io.ReadAll(io.LimitReader(response.Body, maxFCSKeySize+1)) + if err != nil { + return nil, fmt.Errorf("read Apple FCS key: %w", err) + } + + if len(pemData) > maxFCSKeySize { + return nil, errors.New("apple FCS key response is too large") + } + + block, _ := pem.Decode(pemData) + if block == nil { + return nil, errors.New("apple FCS key response is not PEM encoded") + } + + privateKeyValue, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse Apple FCS private key: %w", err) + } + + privateKey, ok := privateKeyValue.(*ecdsa.PrivateKey) + if !ok || privateKey.Curve != elliptic.P256() { + return nil, errors.New("apple FCS key is not a P-256 private key") + } + + responseData, ok := metadata["com.apple.wkms.fcs-response"] + if !ok { + return nil, errors.New("AEA metadata does not contain an FCS response") + } + + var wrapped fcsResponse + if err := json.Unmarshal(responseData, &wrapped); err != nil { + return nil, fmt.Errorf("parse AEA FCS response: %w", err) + } + + encapsulation, err := base64.StdEncoding.DecodeString(wrapped.EncRequest) + if err != nil { + return nil, fmt.Errorf("decode AEA FCS encapsulation: %w", err) + } + + wrappedKey, err := base64.StdEncoding.DecodeString(wrapped.WrappedKey) + if err != nil { + return nil, fmt.Errorf("decode AEA wrapped key: %w", err) + } + + kem := hpke.DHKEM(ecdh.P256()) + ecdhPrivateKey, err := privateKey.ECDH() + if err != nil { + return nil, fmt.Errorf("convert AEA FCS private key: %w", err) + } + hpkePrivateKey, err := kem.NewPrivateKey(ecdhPrivateKey.Bytes()) + if err != nil { + return nil, fmt.Errorf("load AEA FCS private key: %w", err) + } + + recipient, err := hpke.NewRecipient(encapsulation, hpkePrivateKey, hpke.HKDFSHA256(), hpke.AES256GCM(), nil) + if err != nil { + return nil, fmt.Errorf("initialize AEA FCS recipient: %w", err) + } + + symmetricKey, err := recipient.Open(nil, wrappedKey) + if err != nil { + return nil, fmt.Errorf("unwrap AEA symmetric key: %w", err) + } + + return symmetricKey, nil +} + +func (r *aeaReader) indexClusters(offset int64, clusterMAC aeaMAC, root aeaRootHeader, sourceSize int64) error { + const segmentHeaderSize = 8 + sha256.Size + + headerSize := int64(segmentHeaderSize*root.SegmentsPerCluster + sha256.Size + sha256.Size*root.SegmentsPerCluster) + outputSize := uint64(0) + clusterIndex := uint32(0) + + for outputSize < root.FileSize { + if offset < 0 || headerSize > sourceSize-offset { + return errors.New("AEA cluster header extends beyond the encrypted image") + } + + headerData := make([]byte, headerSize) + if _, err := r.source.ReadAt(headerData, offset); err != nil { + return fmt.Errorf("read AEA cluster %d header: %w", clusterIndex, err) + } + + clusterKey, err := deriveAEAKey(r.mainKey, nil, binary.LittleEndian.AppendUint32([]byte(aeaClusterKeyInfo), clusterIndex), len(r.mainKey)) + if err != nil { + return fmt.Errorf("derive AEA cluster %d key: %w", clusterIndex, err) + } + + clusterHeaderKey, err := deriveAEAHeaderKey(clusterKey, []byte(aeaClusterHeaderKeyInfo)) + if err != nil { + return fmt.Errorf("derive AEA cluster %d header key: %w", clusterIndex, err) + } + + encryptedHeadersSize := int(segmentHeaderSize * root.SegmentsPerCluster) + encryptedHeaders := headerData[:encryptedHeadersSize] + var nextClusterMAC aeaMAC + copy(nextClusterMAC[:], headerData[encryptedHeadersSize:encryptedHeadersSize+sha256.Size]) + segmentMACData := headerData[encryptedHeadersSize+sha256.Size:] + + hmacSalt := make([]byte, 0, len(nextClusterMAC)+len(segmentMACData)) + hmacSalt = append(hmacSalt, nextClusterMAC[:]...) + hmacSalt = append(hmacSalt, segmentMACData...) + computedMAC := calculateAEAMAC(clusterHeaderKey.MAC[:], encryptedHeaders, hmacSalt) + if !hmac.Equal(clusterMAC[:], computedMAC[:]) { + return fmt.Errorf("AEA cluster %d failed authentication", clusterIndex) + } + + decryptedHeaders, err := decryptAEACTR(encryptedHeaders, clusterHeaderKey.Key[:], clusterHeaderKey.IV[:]) + if err != nil { + return fmt.Errorf("decrypt AEA cluster %d headers: %w", clusterIndex, err) + } + + dataOffset := offset + headerSize + for segmentIndex := range root.SegmentsPerCluster { + start := int(segmentIndex) * segmentHeaderSize + var segmentHeader aeaSegmentHeader + if err := binary.Read(bytes.NewReader(decryptedHeaders[start:start+segmentHeaderSize]), binary.LittleEndian, &segmentHeader); err != nil { + return fmt.Errorf("decode AEA cluster %d segment %d header: %w", clusterIndex, segmentIndex, err) + } + + if segmentHeader.DecompressedSize == 0 { + if segmentHeader.CompressedSize != 0 { + return fmt.Errorf("AEA cluster %d segment %d has compressed data without output", clusterIndex, segmentIndex) + } + + continue + } + + if segmentHeader.DecompressedSize > root.SegmentSize || segmentHeader.CompressedSize == 0 { + return fmt.Errorf("AEA cluster %d segment %d has invalid sizes", clusterIndex, segmentIndex) + } + + if int64(segmentHeader.CompressedSize) > sourceSize-dataOffset { + return fmt.Errorf("AEA cluster %d segment %d extends beyond the encrypted image", clusterIndex, segmentIndex) + } + + var segmentMAC aeaMAC + copy(segmentMAC[:], segmentMACData[int(segmentIndex)*sha256.Size:(int(segmentIndex)+1)*sha256.Size]) + + globalIndex := uint64(clusterIndex)*uint64(root.SegmentsPerCluster) + uint64(segmentIndex) + if globalIndex >= uint64(len(r.segments)) { + return fmt.Errorf("AEA cluster %d segment %d is outside the declared output", clusterIndex, segmentIndex) + } + + outputOffset := int64(globalIndex * uint64(root.SegmentSize)) + if uint64(outputOffset)+uint64(segmentHeader.DecompressedSize) > root.FileSize { + return fmt.Errorf("AEA cluster %d segment %d extends beyond the declared output", clusterIndex, segmentIndex) + } + + r.segments[globalIndex] = aeaSegment{ + inputOffset: dataOffset, + outputOffset: outputOffset, + compressedSize: segmentHeader.CompressedSize, + decompressedSize: segmentHeader.DecompressedSize, + checksum: segmentHeader.Checksum, + mac: segmentMAC, + cluster: clusterIndex, + index: segmentIndex, + } + + dataOffset += int64(segmentHeader.CompressedSize) + outputSize += uint64(segmentHeader.DecompressedSize) + if outputSize > root.FileSize { + return errors.New("AEA segment data exceeds the declared output size") + } + } + + offset = dataOffset + clusterMAC = nextClusterMAC + clusterIndex++ + } + + if outputSize != root.FileSize { + return fmt.Errorf("AEA segment data has size %d, expected %d", outputSize, root.FileSize) + } + + return nil +} + +func (r *aeaReader) ReadAt(data []byte, offset int64) (int, error) { + if len(data) == 0 { + return 0, nil + } + + if offset < 0 { + return 0, errors.New("negative AEA output offset") + } + + if offset >= r.size { + return 0, io.EOF + } + + wanted := len(data) + if remaining := r.size - offset; int64(wanted) > remaining { + wanted = int(remaining) + } + + written := 0 + for written < wanted { + segmentIndex := int((offset + int64(written)) / int64(r.segmentSize)) + if segmentIndex >= len(r.segments) { + return written, errors.New("AEA segment index is incomplete") + } + + segment := r.segments[segmentIndex] + if segment.decompressedSize == 0 { + return written, fmt.Errorf("AEA segment %d is missing", segmentIndex) + } + decoded, err := r.decodeSegment(segmentIndex, segment) + if err != nil { + return written, err + } + + inside := int(offset + int64(written) - segment.outputOffset) + if inside < 0 || inside >= len(decoded) { + return written, errors.New("AEA segment has an invalid output offset") + } + + count := min(wanted-written, len(decoded)-inside) + copy(data[written:written+count], decoded[inside:inside+count]) + written += count + } + + if wanted != len(data) { + return written, io.EOF + } + + return written, nil +} + +func (r *aeaReader) decodeSegment(cacheIndex int, segment aeaSegment) ([]byte, error) { + r.cacheMu.Lock() + defer r.cacheMu.Unlock() + + if data, ok := r.cache[cacheIndex]; ok { + return data, nil + } + + encrypted := make([]byte, segment.compressedSize) + if _, err := r.source.ReadAt(encrypted, segment.inputOffset); err != nil { + return nil, fmt.Errorf("read AEA cluster %d segment %d: %w", segment.cluster, segment.index, err) + } + + clusterKey, err := deriveAEAKey(r.mainKey, nil, binary.LittleEndian.AppendUint32([]byte(aeaClusterKeyInfo), segment.cluster), len(r.mainKey)) + if err != nil { + return nil, fmt.Errorf("derive AEA cluster %d key: %w", segment.cluster, err) + } + + segmentKey, err := deriveAEAHeaderKey(clusterKey, binary.LittleEndian.AppendUint32([]byte(aeaSegmentKeyInfo), segment.index)) + if err != nil { + return nil, fmt.Errorf("derive AEA cluster %d segment %d key: %w", segment.cluster, segment.index, err) + } + + computedMAC := calculateAEAMAC(segmentKey.MAC[:], encrypted, nil) + if !hmac.Equal(segment.mac[:], computedMAC[:]) { + return nil, fmt.Errorf("AEA cluster %d segment %d failed authentication", segment.cluster, segment.index) + } + + compressed, err := decryptAEACTR(encrypted, segmentKey.Key[:], segmentKey.IV[:]) + if err != nil { + return nil, fmt.Errorf("decrypt AEA cluster %d segment %d: %w", segment.cluster, segment.index, err) + } + + var decoded []byte + if segment.compressedSize == segment.decompressedSize { + decoded = compressed + } else { + decoded, err = lzfse.Decompress(compressed) + if err != nil { + return nil, fmt.Errorf("decompress AEA cluster %d segment %d: %w", segment.cluster, segment.index, err) + } + if len(decoded) != int(segment.decompressedSize) { + return nil, fmt.Errorf("decompress AEA cluster %d segment %d: produced %d bytes, expected %d", segment.cluster, segment.index, len(decoded), segment.decompressedSize) + } + } + + if sha256.Sum256(decoded) != segment.checksum { + return nil, fmt.Errorf("AEA cluster %d segment %d failed checksum verification", segment.cluster, segment.index) + } + + if len(r.cacheOrder) == aeaDecodedSegmentCacheCount { + delete(r.cache, r.cacheOrder[0]) + copy(r.cacheOrder, r.cacheOrder[1:]) + r.cacheOrder = r.cacheOrder[:len(r.cacheOrder)-1] + } + + r.cache[cacheIndex] = decoded + r.cacheOrder = append(r.cacheOrder, cacheIndex) + + return decoded, nil +} + +func deriveAEAKey(key, salt, info []byte, size int) ([]byte, error) { + derived, err := hkdf.Key(sha256.New, key, salt, string(info), size) + if err != nil { + return nil, fmt.Errorf("expand AEA key: %w", err) + } + + return derived, nil +} + +func deriveAEAHeaderKey(key, info []byte) (aeaHeaderKey, error) { + var headerKey aeaHeaderKey + derived, err := deriveAEAKey(key, nil, info, binary.Size(headerKey)) + if err != nil { + return aeaHeaderKey{}, fmt.Errorf("derive AEA header key: %w", err) + } + + if err := binary.Read(bytes.NewReader(derived), binary.LittleEndian, &headerKey); err != nil { + return aeaHeaderKey{}, fmt.Errorf("decode AEA header key: %w", err) + } + + return headerKey, nil +} + +func calculateAEAMAC(key, data, salt []byte) aeaMAC { + mac := hmac.New(sha256.New, key) + _, _ = mac.Write(salt) + _, _ = mac.Write(data) + + var length [8]byte + binary.LittleEndian.PutUint64(length[:], uint64(len(salt))) + _, _ = mac.Write(length[:]) + + return aeaMAC(mac.Sum(nil)) +} + +func decryptAEACTR(data, key, iv []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("initialize AEA cipher: %w", err) + } + + decrypted := make([]byte, len(data)) + cipher.NewCTR(block, iv).XORKeyStream(decrypted, data) + + return decrypted, nil +} + +func readBinaryAt(source io.ReaderAt, offset int64, value any) error { + size := binary.Size(value) + if size < 0 { + return errors.New("invalid binary value") + } + + data := make([]byte, size) + if _, err := source.ReadAt(data, offset); err != nil { + return fmt.Errorf("read binary value: %w", err) + } + + if err := binary.Read(bytes.NewReader(data), binary.LittleEndian, value); err != nil { + return fmt.Errorf("decode binary value: %w", err) + } + + return nil +} diff --git a/internal/sap/assets/apfs.go b/internal/sap/assets/apfs.go new file mode 100644 index 00000000..90c8d934 --- /dev/null +++ b/internal/sap/assets/apfs.go @@ -0,0 +1,864 @@ +//nolint:wsl // Binary tree traversal is clearer when related bounds checks remain adjacent. +package assets + +import ( + "bufio" + "bytes" + "context" + "crypto/sha256" + "encoding/binary" + "fmt" + "io" + "math" + "os" + "path/filepath" + "sort" + "strings" + "sync" + + "github.com/blacktop/go-apfs" + "github.com/blacktop/go-apfs/pkg/disk" + "github.com/blacktop/go-apfs/pkg/disk/dmg" + "github.com/blacktop/go-apfs/types" + "github.com/deploymenttheory/go-macos-pkg/pkg/lzbitmap" + "github.com/go-compressions/lzfse" +) + +const ( + apfsObjectHeaderSize = 32 + apfsObjectTypeBTree = 2 + apfsObjectTypeNode = 3 + apfsObjectTypeFSTree = 14 + apfsObjectNoHeader = 0x20000000 + apfsNodeRoot = 0x0001 + apfsNodeNoHeader = 0x0010 + apfsBlockSize = 4096 + apfsMaximumTreeDepth = 32 + decmpfsLZVN = 8 + decmpfsLZFSE = 12 + decmpfsLZBitmap = 14 + decmpfsBlockSize = 64 << 10 +) + +type apfsExtractor struct { + device disk.Device + filesystem *apfs.APFS + volumeOMap types.BTreeNodePhys + rootTreeBase uint64 + transactionID types.XidT +} + +type apfsExtent struct { + logical uint64 + physical uint64 + length uint64 +} + +type apfsFile struct { + io.ReaderAt + size int64 +} + +func extractAPFSFiles(ctx context.Context, source io.ReaderAt, size int64, paths []string, destination string) error { + extractor, closeExtractor, err := newAPFSExtractor(source, size) + if err != nil { + return err + } + defer func() { _ = closeExtractor() }() + + if err := os.MkdirAll(destination, 0o700); err != nil { + return fmt.Errorf("create Apple filesystem extraction directory: %w", err) + } + + for _, path := range paths { + if err := ctx.Err(); err != nil { + return fmt.Errorf("extract Apple filesystem files: %w", err) + } + if err := extractor.extractFile(ctx, path, destination); err != nil { + return fmt.Errorf("extract %s from Apple filesystem: %w", path, err) + } + } + + return nil +} + +func newAPFSExtractor(source io.ReaderAt, size int64) (*apfsExtractor, func() error, error) { + device, closeDevice, err := openAPFSDevice(source, size) + if err != nil { + return nil, nil, err + } + + filesystem, err := apfs.NewAPFS(device) + if err != nil { + _ = closeDevice() + + return nil, nil, fmt.Errorf("open Apple filesystem: %w", err) + } + fail := func(err error) (*apfsExtractor, func() error, error) { + filesystem.Close() + _ = closeDevice() + + return nil, nil, err + } + + volumeOMap, ok := filesystem.Volume.OMap.Body.(types.OMap) + if !ok { + return fail(fmt.Errorf("apple filesystem volume has an invalid object map")) + } + if volumeOMap.Tree == nil { + return fail(fmt.Errorf("apple filesystem volume has no object-map tree")) + } + volumeOMapRoot, ok := volumeOMap.Tree.Body.(types.BTreeNodePhys) + if !ok { + return fail(fmt.Errorf("apple filesystem volume has an invalid object-map tree")) + } + + extractor := &apfsExtractor{ + device: device, + filesystem: filesystem, + volumeOMap: volumeOMapRoot, + rootTreeBase: uint64(filesystem.Volume.RootTreeOid), + transactionID: filesystem.Volume.OMap.Hdr.Xid, + } + closeExtractor := func() error { + filesystem.Close() + + return closeDevice() + } + + return extractor, closeExtractor, nil +} + +func (e *apfsExtractor) extractFile(ctx context.Context, path, destination string) (err error) { + file, err := e.openFile(ctx, path) + if err != nil { + return err + } + + outputPath := filepath.Join(destination, filepath.Base(path)) + output, err := os.OpenFile(outputPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) + if err != nil { + return fmt.Errorf("create output: %w", err) + } + defer func() { + if closeErr := output.Close(); err == nil && closeErr != nil { + err = fmt.Errorf("close output: %w", closeErr) + } + + if err != nil { + _ = os.Remove(outputPath) + } + }() + + reader := &contextReader{ctx: ctx, reader: io.NewSectionReader(file, 0, file.size)} + if _, err := io.CopyN(output, reader, file.size); err != nil { + return fmt.Errorf("copy file data: %w", err) + } + + if err := output.Sync(); err != nil { + return fmt.Errorf("sync output: %w", err) + } + + return nil +} + +func (e *apfsExtractor) openFile(ctx context.Context, path string) (*apfsFile, error) { + records, err := e.resolvePath(path) + if err != nil { + return nil, err + } + + var inode *types.JInodeVal + var compression *types.DecmpfsDiskHeader + var resource *types.JXattrDstreamT + var extents []apfsExtent + for _, record := range records { + switch record.Hdr.GetType() { + case types.APFS_TYPE_INODE: + value, ok := record.Val.(types.JInodeVal) + if ok { + inode = &value + } + case types.APFS_TYPE_FILE_EXTENT: + key, keyOK := record.Key.(types.JFileExtentKeyT) + value, valueOK := record.Val.(types.JFileExtentValT) + if keyOK && valueOK { + extents = append(extents, apfsExtent{ + logical: key.LogicalAddr, + physical: value.PhysBlockNum, + length: value.Length(), + }) + } + case types.APFS_TYPE_XATTR: + key, keyOK := record.Key.(types.JXattrKeyT) + value, valueOK := record.Val.(types.JXattrValT) + if !keyOK || !valueOK { + continue + } + + switch key.Name { + case types.DECMPFS_XATTR_NAME: + compression, err = types.GetDecmpfsHeader(record) + if err != nil { + return nil, fmt.Errorf("read compression metadata: %w", err) + } + case "com.apple.ResourceFork": + stream, ok := value.Data.(types.JXattrDstreamT) + if ok { + resource = &stream + } + } + } + } + + if inode == nil { + return nil, fmt.Errorf("file has no inode record") + } + + if compression != nil { + method := uint32(compression.CompressionType) + if method != decmpfsLZVN && method != decmpfsLZFSE && method != decmpfsLZBitmap { + return nil, fmt.Errorf("unsupported compression type %d", compression.CompressionType) + } + if resource == nil { + return nil, fmt.Errorf("compressed file has no resource fork") + } + + extents, err = e.dataExtents(resource.XattrObjID) + if err != nil { + return nil, fmt.Errorf("read resource-fork extents: %w", err) + } + resourceReader, err := newAPFSExtentReader(e.device, extents, resource.DStream.Size) + if err != nil { + return nil, err + } + reader, err := newDecmpfsReader(ctx, resourceReader, resource.DStream.Size, compression.UncompressedSize, method) + if err != nil { + return nil, err + } + + return &apfsFile{ReaderAt: reader, size: int64(compression.UncompressedSize)}, nil + } + + size := inode.UncompressedSize + for _, field := range inode.Xfields { + if stream, ok := field.Field.(types.JDstreamT); ok { + size = stream.Size + + break + } + } + if len(extents) == 0 && size != 0 { + extents, err = e.dataExtents(inode.PrivateID) + if err != nil { + return nil, fmt.Errorf("read file extents: %w", err) + } + } + reader, err := newAPFSExtentReader(e.device, extents, size) + if err != nil { + return nil, err + } + + return &apfsFile{ReaderAt: reader, size: int64(size)}, nil +} + +func (e *apfsExtractor) resolvePath(path string) ([]types.NodeEntry, error) { + objectID := uint64(types.FSROOT_OID) + for _, name := range strings.Split(strings.Trim(path, "/"), "/") { + records, err := e.recordsForObject(objectID) + if err != nil { + return nil, err + } + + found := false + for _, record := range records { + if record.Hdr.GetType() != types.APFS_TYPE_DIR_REC { + continue + } + key, keyOK := record.Key.(types.JDrecHashedKeyT) + value, valueOK := record.Val.(types.JDrecVal) + if keyOK && valueOK && key.Name == name { + objectID = value.FileID + found = true + + break + } + } + if !found { + return nil, fmt.Errorf("path component %q was not found", name) + } + } + + return e.recordsForObject(objectID) +} + +func (e *apfsExtractor) recordsForObject(objectID uint64) ([]types.NodeEntry, error) { + visited := make(map[uint64]struct{}) + + return e.collectRecords(e.filesystem.FSRootBtree, objectID, visited, 0) +} + +func (e *apfsExtractor) collectRecords(node types.BTreeNodePhys, objectID uint64, visited map[uint64]struct{}, depth int) ([]types.NodeEntry, error) { + if depth > apfsMaximumTreeDepth { + return nil, fmt.Errorf("filesystem tree exceeds maximum depth") + } + if node.IsLeaf() { + var records []types.NodeEntry + for _, raw := range node.Entries { + record, ok := raw.(types.NodeEntry) + if ok && record.Hdr.GetID() == objectID { + records = append(records, record) + } + } + + return records, nil + } + + var records []types.NodeEntry + for index, raw := range node.Entries { + entry, ok := raw.(types.NodeEntry) + if !ok { + return nil, fmt.Errorf("filesystem tree contains an invalid index entry") + } + lower := entry.Hdr.GetID() + upper := ^uint64(0) + if index+1 < len(node.Entries) { + next, ok := node.Entries[index+1].(types.NodeEntry) + if !ok { + return nil, fmt.Errorf("filesystem tree contains an invalid index entry") + } + upper = next.Hdr.GetID() + } + if objectID < lower || objectID > upper { + continue + } + + child, address, err := e.filesystemChild(entry) + if err != nil { + return nil, err + } + if _, seen := visited[address]; seen { + continue + } + visited[address] = struct{}{} + childRecords, err := e.collectRecords(child, objectID, visited, depth+1) + if err != nil { + return nil, err + } + records = append(records, childRecords...) + } + + return records, nil +} + +func (e *apfsExtractor) filesystemChild(entry types.NodeEntry) (types.BTreeNodePhys, uint64, error) { + var objectID types.OidT + var expectedHash *[sha256.Size]byte + switch value := entry.Val.(type) { + case types.BTreeNodeIndexNodeValT: + objectID = types.OidT(e.rootTreeBase + uint64(value.ChildOid)) + expectedHash = &value.ChildHash + case uint64: + objectID = types.OidT(value) + default: + return types.BTreeNodePhys{}, 0, fmt.Errorf("sealed filesystem tree contains an invalid child reference") + } + + mapping, err := e.volumeOMap.GetOMapEntry(e.device, objectID, e.transactionID) + if err != nil { + return types.BTreeNodePhys{}, 0, fmt.Errorf("resolve filesystem child %#x: %w", objectID, err) + } + + if expectedHash != nil { + raw := make([]byte, apfsBlockSize) + if _, err := readAPFSBlock(e.device, raw, mapping.Val.Paddr); err != nil { + return types.BTreeNodePhys{}, 0, fmt.Errorf("read filesystem child %#x: %w", objectID, err) + } + if digest := sha256.Sum256(raw); digest != *expectedHash { + return types.BTreeNodePhys{}, 0, fmt.Errorf("filesystem child %#x failed SHA-256 verification", objectID) + } + } + + object, err := types.ReadObj(e.device, mapping.Val.Paddr) + if err != nil { + return types.BTreeNodePhys{}, 0, fmt.Errorf("decode filesystem child %#x: %w", objectID, err) + } + child, ok := object.Body.(types.BTreeNodePhys) + if !ok { + return types.BTreeNodePhys{}, 0, fmt.Errorf("filesystem child %#x is not a tree node", objectID) + } + + return child, mapping.Val.Paddr, nil +} + +func (e *apfsExtractor) physicalExtents(objectID uint64) ([]apfsExtent, error) { + object, err := types.ReadObj(e.device, uint64(e.filesystem.Volume.FextTreeOid)) + if err != nil { + return nil, fmt.Errorf("read physical extent tree: %w", err) + } + root, ok := object.Body.(types.BTreeNodePhys) + if !ok { + return nil, fmt.Errorf("physical extent tree root is not a tree node") + } + + extents, err := e.collectPhysicalExtents(root, objectID, make(map[uint64]struct{}), 0) + if err != nil { + return nil, err + } + sort.Slice(extents, func(i, j int) bool { return extents[i].logical < extents[j].logical }) + + return extents, nil +} + +func (e *apfsExtractor) dataExtents(objectID uint64) ([]apfsExtent, error) { + if e.filesystem.Volume.FextTreeOid != 0 { + return e.physicalExtents(objectID) + } + + records, err := e.recordsForObject(objectID) + if err != nil { + return nil, err + } + var extents []apfsExtent + for _, record := range records { + if record.Hdr.GetType() != types.APFS_TYPE_FILE_EXTENT { + continue + } + key, keyOK := record.Key.(types.JFileExtentKeyT) + value, valueOK := record.Val.(types.JFileExtentValT) + if keyOK && valueOK { + extents = append(extents, apfsExtent{ + logical: key.LogicalAddr, + physical: value.PhysBlockNum, + length: value.Length(), + }) + } + } + sort.Slice(extents, func(i, j int) bool { return extents[i].logical < extents[j].logical }) + + return extents, nil +} + +func (e *apfsExtractor) collectPhysicalExtents(node types.BTreeNodePhys, objectID uint64, visited map[uint64]struct{}, depth int) ([]apfsExtent, error) { + if depth > apfsMaximumTreeDepth { + return nil, fmt.Errorf("physical extent tree exceeds maximum depth") + } + if node.IsLeaf() { + var extents []apfsExtent + for _, raw := range node.Entries { + entry, ok := raw.(types.FextNodeEntry) + if ok && entry.Key.PrivateID == objectID { + extents = append(extents, apfsExtent{ + logical: entry.Key.LogicalAddr, + physical: entry.Val.PhysBlockNum, + length: entry.Val.Length(), + }) + } + } + + return extents, nil + } + + var extents []apfsExtent + for index, raw := range node.Entries { + entry, ok := raw.(types.OMapNodeEntry) + if !ok { + return nil, fmt.Errorf("physical extent tree contains an invalid index entry") + } + lower := uint64(entry.Key.Oid) + upper := ^uint64(0) + if index+1 < len(node.Entries) { + next, ok := node.Entries[index+1].(types.OMapNodeEntry) + if !ok { + return nil, fmt.Errorf("physical extent tree contains an invalid index entry") + } + upper = uint64(next.Key.Oid) + } + if objectID < lower || objectID > upper { + continue + } + if _, seen := visited[entry.PAddr]; seen { + continue + } + visited[entry.PAddr] = struct{}{} + + object, err := types.ReadObj(e.device, entry.PAddr) + if err != nil { + return nil, fmt.Errorf("read physical extent child %#x: %w", entry.PAddr, err) + } + child, ok := object.Body.(types.BTreeNodePhys) + if !ok { + return nil, fmt.Errorf("physical extent child %#x is not a tree node", entry.PAddr) + } + childExtents, err := e.collectPhysicalExtents(child, objectID, visited, depth+1) + if err != nil { + return nil, err + } + extents = append(extents, childExtents...) + } + + return extents, nil +} + +func readAPFSBlock(device disk.Device, data []byte, physicalAddress uint64) (int, error) { + var count int + var err error + if raw, ok := device.(*readerDevice); ok { + count, err = raw.source.ReadAt(data, int64(physicalAddress*apfsBlockSize)) + } else { + count, err = device.ReadAt(data, int64(physicalAddress*apfsBlockSize)) + } + if err != nil { + return count, fmt.Errorf("read Apple filesystem block: %w", err) + } + + return count, nil +} + +type apfsExtentReader struct { + device disk.Device + extents []apfsExtent + size uint64 +} + +func newAPFSExtentReader(device disk.Device, extents []apfsExtent, size uint64) (*apfsExtentReader, error) { + if size > math.MaxInt64 { + return nil, fmt.Errorf("file size %d exceeds the supported maximum", size) + } + + sort.Slice(extents, func(i, j int) bool { return extents[i].logical < extents[j].logical }) + position := uint64(0) + for _, extent := range extents { + if extent.length == 0 || extent.logical != position || extent.physical > ^uint64(0)/apfsBlockSize { + return nil, fmt.Errorf("file has invalid or sparse physical extents") + } + physicalOffset := extent.physical * apfsBlockSize + if physicalOffset > device.GetSize() || extent.length > device.GetSize()-physicalOffset { + return nil, fmt.Errorf("file extent exceeds the Apple filesystem image") + } + position += extent.length + if position < extent.logical { + return nil, fmt.Errorf("file extent length overflows") + } + } + if position < size { + return nil, fmt.Errorf("file extents cover %d bytes, expected %d", position, size) + } + + return &apfsExtentReader{device: device, extents: extents, size: size}, nil +} + +func (r *apfsExtentReader) ReadAt(data []byte, offset int64) (int, error) { + if offset < 0 { + return 0, fmt.Errorf("negative file offset") + } + if len(data) == 0 { + return 0, nil + } + position := uint64(offset) + if position >= r.size { + return 0, io.EOF + } + + written := 0 + for written < len(data) && position < r.size { + index := sort.Search(len(r.extents), func(index int) bool { + extent := r.extents[index] + + return extent.logical+extent.length > position + }) + if index == len(r.extents) || position < r.extents[index].logical { + return written, io.ErrUnexpectedEOF + } + extent := r.extents[index] + available := min(extent.logical+extent.length-position, r.size-position) + count := min(uint64(len(data)-written), available) + physicalOffset := extent.physical*apfsBlockSize + position - extent.logical + n, err := r.device.ReadAt(data[written:written+int(count)], int64(physicalOffset)) + written += n + position += uint64(n) + if err != nil { + return written, err //nolint:wrapcheck // ReaderAt implementations must preserve io.EOF. + } + if n != int(count) { + return written, io.ErrUnexpectedEOF + } + } + if written != len(data) { + return written, io.EOF + } + + return written, nil +} + +type decmpfsReader struct { + ctx context.Context + source io.ReaderAt + outputSize uint64 + method uint32 + offsets []uint32 + mu sync.Mutex + cache map[uint64][]byte + cacheOrder []uint64 +} + +func newDecmpfsReader(ctx context.Context, source io.ReaderAt, sourceSize, outputSize uint64, method uint32) (*decmpfsReader, error) { + if outputSize > math.MaxInt64 { + return nil, fmt.Errorf("compressed file size %d exceeds the supported maximum", outputSize) + } + if sourceSize < 8 { + return nil, fmt.Errorf("compressed resource fork is too small") + } + + var first [4]byte + if _, err := source.ReadAt(first[:], 0); err != nil { + return nil, fmt.Errorf("read compression index: %w", err) + } + tableSize := uint64(binary.LittleEndian.Uint32(first[:])) + blockCount := (outputSize + decmpfsBlockSize - 1) / decmpfsBlockSize + expectedTableSize := (blockCount + 1) * 4 + if tableSize != expectedTableSize || tableSize > sourceSize { + return nil, fmt.Errorf("compression index has size %d, expected %d", tableSize, expectedTableSize) + } + + table := make([]byte, tableSize) + if _, err := source.ReadAt(table, 0); err != nil { + return nil, fmt.Errorf("read compression index: %w", err) + } + offsets := make([]uint32, blockCount+1) + for index := range offsets { + offsets[index] = binary.LittleEndian.Uint32(table[index*4:]) + } + for index := uint64(0); index < blockCount; index++ { + start, end := uint64(offsets[index]), uint64(offsets[index+1]) + if start < tableSize || end <= start || end > sourceSize || end-start > decmpfsBlockSize+1 { + return nil, fmt.Errorf("compressed block %d has invalid range %d..%d", index, start, end) + } + } + if uint64(offsets[len(offsets)-1]) != sourceSize { + return nil, fmt.Errorf("compressed blocks end at %d, expected %d", offsets[len(offsets)-1], sourceSize) + } + + return &decmpfsReader{ + ctx: ctx, + source: source, + outputSize: outputSize, + method: method, + offsets: offsets, + cache: make(map[uint64][]byte), + }, nil +} + +func (r *decmpfsReader) ReadAt(data []byte, offset int64) (int, error) { + if offset < 0 { + return 0, fmt.Errorf("negative compressed file offset") + } + if len(data) == 0 { + return 0, nil + } + if err := r.ctx.Err(); err != nil { + return 0, err //nolint:wrapcheck // ReaderAt must preserve context cancellation. + } + position := uint64(offset) + if position >= r.outputSize { + return 0, io.EOF + } + + wanted := len(data) + if remaining := r.outputSize - position; uint64(wanted) > remaining { + wanted = int(remaining) + } + written := 0 + for written < wanted { + blockIndex := position / decmpfsBlockSize + block, err := r.block(blockIndex) + if err != nil { + return written, err + } + blockOffset := position % decmpfsBlockSize + count := min(wanted-written, len(block)-int(blockOffset)) + copy(data[written:written+count], block[blockOffset:uint64(count)+blockOffset]) + written += count + position += uint64(count) + } + if written != len(data) { + return written, io.EOF + } + + return written, nil +} + +func (r *decmpfsReader) block(index uint64) ([]byte, error) { + r.mu.Lock() + defer r.mu.Unlock() + + if block, ok := r.cache[index]; ok { + return block, nil + } + if index+1 >= uint64(len(r.offsets)) { + return nil, io.EOF + } + if err := r.ctx.Err(); err != nil { + return nil, fmt.Errorf("read compressed file: %w", err) + } + + start, end := uint64(r.offsets[index]), uint64(r.offsets[index+1]) + compressed := make([]byte, end-start) + if _, err := r.source.ReadAt(compressed, int64(start)); err != nil { + return nil, fmt.Errorf("read compressed block %d: %w", index, err) + } + expected := min(uint64(decmpfsBlockSize), r.outputSize-index*decmpfsBlockSize) + plain, err := decompressBlock(compressed, r.method, int(expected)) + if err != nil { + return nil, fmt.Errorf("decode compressed block %d: %w", index, err) + } + if uint64(len(plain)) != expected { + return nil, fmt.Errorf("decompressed block %d has size %d, expected %d", index, len(plain), expected) + } + + const maximumCachedBlocks = 128 + if len(r.cacheOrder) == maximumCachedBlocks { + delete(r.cache, r.cacheOrder[0]) + r.cacheOrder = r.cacheOrder[1:] + } + r.cache[index] = plain + r.cacheOrder = append(r.cacheOrder, index) + + return plain, nil +} + +func decompressBlock(compressed []byte, method uint32, maximumSize int) ([]byte, error) { + switch method { + case decmpfsLZVN: + if compressed[0] == 0x06 { + return compressed[1:], nil + } + + decoded, err := lzfse.DecompressLZVN(compressed, maximumSize) + if err != nil { + return nil, fmt.Errorf("decompress LZVN block: %w", err) + } + + return decoded, nil + case decmpfsLZFSE: + if compressed[0] != 'b' { + return compressed[1:], nil + } + + decoded, err := lzfse.Decompress(compressed) + if err != nil { + return nil, fmt.Errorf("decompress LZFSE block: %w", err) + } + + return decoded, nil + case decmpfsLZBitmap: + if compressed[0] == 0xff { + return compressed[1:], nil + } + + decoded, err := lzbitmap.Decompress(compressed) + if err != nil { + return nil, fmt.Errorf("decompress LZBITMAP block: %w", err) + } + + return decoded, nil + default: + return nil, fmt.Errorf("unsupported compression type %d", method) + } +} + +type contextReader struct { + ctx context.Context + reader io.Reader +} + +func (r *contextReader) Read(data []byte) (int, error) { + if err := r.ctx.Err(); err != nil { + return 0, err //nolint:wrapcheck // Preserve context cancellation for io.CopyN. + } + + return r.reader.Read(data) //nolint:wrapcheck // Reader adapters must preserve io.EOF. +} + +func openAPFSDevice(source io.ReaderAt, size int64) (disk.Device, func() error, error) { + if size < 36 { + return nil, nil, fmt.Errorf("apple filesystem image is too small: %d", size) + } + + header := make([]byte, 36) + if _, err := source.ReadAt(header, 0); err != nil { + return nil, nil, fmt.Errorf("read Apple filesystem header: %w", err) + } + + if bytes.Equal(header[32:36], []byte("NXSB")) { + device := &readerDevice{source: source, size: uint64(size)} + + return device, device.Close, nil + } + + image, err := dmg.NewDMG(io.NewSectionReader(source, 0, size)) + if err != nil { + return nil, nil, fmt.Errorf("open Apple disk image: %w", err) + } + + return image, image.Close, nil +} + +type readerDevice struct { + source io.ReaderAt + size uint64 +} + +func (d *readerDevice) Close() error { + return nil +} + +func (d *readerDevice) ReadFile(writer *bufio.Writer, offset, length int64) error { + if offset < 0 || length < 0 || uint64(offset) > d.size || uint64(length) > d.size-uint64(offset) { + return fmt.Errorf("invalid Apple filesystem range %d+%d", offset, length) + } + + if _, err := io.CopyN(writer, io.NewSectionReader(d.source, offset, length), length); err != nil { + return fmt.Errorf("copy Apple filesystem range: %w", err) + } + + if err := writer.Flush(); err != nil { + return fmt.Errorf("flush Apple filesystem range: %w", err) + } + + return nil +} + +func (d *readerDevice) GetSize() uint64 { + return d.size +} + +func (d *readerDevice) ReadAt(data []byte, offset int64) (int, error) { + count, err := d.source.ReadAt(data, offset) + if count != len(data) || len(data) != int(types.BLOCK_SIZE) || offset%int64(types.BLOCK_SIZE) != 0 { + return count, err //nolint:wrapcheck // ReaderAt implementations must preserve io.EOF. + } + + if !bytes.Equal(data[:apfsObjectHeaderSize], make([]byte, apfsObjectHeaderSize)) { + return count, err //nolint:wrapcheck // ReaderAt implementations must preserve io.EOF. + } + + flags := binary.LittleEndian.Uint16(data[apfsObjectHeaderSize:]) + if flags&apfsNodeNoHeader == 0 { + return count, err //nolint:wrapcheck // ReaderAt implementations must preserve io.EOF. + } + + objectType := uint32(apfsObjectTypeNode) + if flags&apfsNodeRoot != 0 { + objectType = apfsObjectTypeBTree + } + + // Sealed APFS tree nodes omit the normal object header. Add a synthetic + // header after their hash has been verified so go-apfs can decode them. + binary.LittleEndian.PutUint64(data[8:16], uint64(offset/int64(types.BLOCK_SIZE))) + binary.LittleEndian.PutUint32(data[24:28], apfsObjectNoHeader|objectType) + binary.LittleEndian.PutUint32(data[28:32], apfsObjectTypeFSTree) + binary.LittleEndian.PutUint64(data[:8], types.CreateChecksum(data)) + + return count, err //nolint:wrapcheck // ReaderAt implementations must preserve io.EOF. +} diff --git a/internal/sap/assets/assets.go b/internal/sap/assets/assets.go index ac698793..c9bec964 100644 --- a/internal/sap/assets/assets.go +++ b/internal/sap/assets/assets.go @@ -1,8 +1,6 @@ package assets import ( - "bytes" - "compress/bzip2" "context" "crypto/sha256" "encoding/hex" @@ -10,67 +8,64 @@ import ( "fmt" "io" "io/fs" - "net/http" - "net/url" "os" "path/filepath" - "time" - - "github.com/blacktop/go-macho/pkg/xar" - "howett.net/ranger" - - "github.com/majd/ipatool/v2/internal/sap/cpio" ) const ( - updateURL = "https://swcdn.apple.com/content/downloads/27/34/041-98128-A_SYPWICN3KH/5dqkl4rqgbsr18yzy61yeie9g3cmjc5hiv/OSXUpd10.9.pkg" - payloadName = "Payload" - payloadBZOffset = int64(0x352F40D5) - payloadCPIO = int64(0x3A4) + macOSVersion = "27.0.1" + macOSBuild = "26A434" + macOSRestoreURL = "https://updates.cdn-apple.com/2026FallFCS/59241290-5d51-4ca8-9df4-31624b9a4eac/UniversalMac_27.0.1_26A434_Restore.ipsw" + assetCacheDirectory = "apple-assets-macos-27-26A434-arm64-v1" ) type Bundle struct { - CommerceKit []byte - CommerceCore []byte - CoreFP []byte - CoreFPICXS []byte + AppleMediaServices []byte + Commerce []byte + CoreFP []byte + CoreFPICXS []byte } type fileSpec struct { name string - path string size int digest [32]byte } var requiredFiles = []fileSpec{ { - name: "CommerceKit", - path: "./System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/CommerceKit", - size: 3271840, - digest: mustDigest("b84ff12c21987856c0a17b78f1ad82b73195a6dec5f3b208a17d245555a2c8a2"), + name: "AppleMediaServices", + size: 14813888, + digest: mustDigest("95f4558aa2c0ccd9f96ed479e336b630b46bf4b4e0baabc1c1ac8c7d55e21ab4"), }, { - name: "CommerceCore", - path: "./System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Frameworks/CommerceCore.framework/Versions/A/CommerceCore", - size: 207744, - digest: mustDigest("c5401e57402230f3c876409d295319ddf1e61287bc882683c5d61277be7bc1f2"), + name: "commerce", + size: 8092640, + digest: mustDigest("76de40a4a32f691947cb926ce3b128d2fc446f7cac9755f831c406cf5a316ac9"), }, { name: "CoreFP", - path: "./System/Library/PrivateFrameworks/CoreFP.framework/Versions/A/CoreFP", - size: 29014912, - digest: mustDigest("f19141336be4198d0f8991bb00017c915efc7aeaece36c345f7faa1237ea6074"), + size: 94842464, + digest: mustDigest("2c30fa2b5f695fd66c0360dce72f54757d03ddb7107549715c12cb359a003b46"), }, { name: "CoreFP.icxs", - path: "./System/Library/PrivateFrameworks/CoreFP.framework/Versions/A/CoreFP.icxs", - size: 5288352, - digest: mustDigest("473e78af86979f5bd4f6269561caf770b3d16c098d918846eeac8cdd2fe6566a"), + size: 7365344, + digest: mustDigest("cb8f55330ec567da3e692dab5f5388bed0312dd9093340477c5759078cc2aa7b"), }, } +// Load resolves and caches the pinned macOS framework profile used by SAP, +// kbsync generation, and package decryption. func Load(ctx context.Context) (Bundle, error) { + if ctx == nil { + return Bundle{}, errors.New("apple asset context is nil") + } + + if err := ctx.Err(); err != nil { + return Bundle{}, fmt.Errorf("load Apple assets: %w", err) + } + directory, err := cacheDirectory() if err != nil { return Bundle{}, err @@ -80,7 +75,7 @@ func Load(ctx context.Context) (Bundle, error) { return bundle, nil } - bundle, err := download(ctx) + bundle, err := resolveRemoteImage(ctx) if err != nil { return Bundle{}, err } @@ -92,117 +87,13 @@ func Load(ctx context.Context) (Bundle, error) { return bundle, nil } -func download(ctx context.Context) (Bundle, error) { - found, err := downloadFiles(ctx, requiredFiles, "download Apple SAP assets") - if err != nil { - return Bundle{}, err - } - - bundle := bundleFrom(found) - if err := validate(bundle); err != nil { - return Bundle{}, err - } - - return bundle, nil -} - -func downloadFiles(ctx context.Context, specs []fileSpec, operation string) (map[string][]byte, error) { - parsed, err := url.Parse(updateURL) - if err != nil { - return nil, fmt.Errorf("parse Apple software update URL: %w", err) - } - - client := &http.Client{ - Timeout: 2 * time.Minute, - Transport: contextTransport{ctx: ctx, next: http.DefaultTransport}, - } - - remote, err := ranger.NewReader(&ranger.HTTPRanger{Client: client, URL: parsed}) - if err != nil { - return nil, fmt.Errorf("open Apple software update: %w", err) - } - - length, err := remote.Length() - if err != nil { - return nil, fmt.Errorf("measure Apple software update: %w", err) - } - - container, err := xar.NewReader(remote, length) - if err != nil { - return nil, fmt.Errorf("read Apple software update: %w", err) - } - - var payload *xar.File - - for _, candidate := range container.Files { - if candidate.Name == payloadName { - payload = candidate - - break - } - } - - if payload == nil { - return nil, &fs.PathError{Op: "open", Path: payloadName, Err: fs.ErrNotExist} - } - - raw := payload.OpenRaw() - if _, err := raw.Seek(payloadBZOffset, io.SeekStart); err != nil { - return nil, fmt.Errorf("seek Apple update payload: %w", err) - } - - compressed := io.MultiReader(bytes.NewReader([]byte{'B', 'Z', 'h', '9'}), raw) - - archive := bzip2.NewReader(compressed) - if _, err := io.CopyN(io.Discard, archive, payloadCPIO); err != nil { - return nil, fmt.Errorf("seek Apple payload archive: %w", err) - } - - wanted := make(map[string]fileSpec, len(specs)) - for _, spec := range specs { - wanted[spec.path] = spec - } - - found := make(map[string][]byte, len(specs)) - reader := cpio.NewReader(archive) - - for len(found) != len(wanted) { - if err := ctx.Err(); err != nil { - return nil, fmt.Errorf("%s: %w", operation, err) - } - - path, body, err := reader.Next() - if errors.Is(err, io.EOF) { - break - } - - if err != nil { - return nil, fmt.Errorf("read Apple payload archive: %w", err) - } - - spec, ok := wanted[path] - if !ok { - continue - } - - data, err := io.ReadAll(io.LimitReader(body, int64(spec.size)+1)) - if err != nil { - return nil, fmt.Errorf("read %s: %w", spec.name, err) - } - - found[spec.name] = data - } - - return found, nil -} - func cacheDirectory() (string, error) { root, err := os.UserCacheDir() if err != nil { return "", fmt.Errorf("find user cache directory: %w", err) } - return filepath.Join(root, "ipatool", "sap", "apple-assets-v2"), nil + return filepath.Join(root, "ipatool", "sap", assetCacheDirectory), nil } func readCache(directory string) (Bundle, error) { @@ -338,19 +229,19 @@ func validate(bundle Bundle) error { func bundleFrom(files map[string][]byte) Bundle { return Bundle{ - CommerceKit: files["CommerceKit"], - CommerceCore: files["CommerceCore"], - CoreFP: files["CoreFP"], - CoreFPICXS: files["CoreFP.icxs"], + AppleMediaServices: files["AppleMediaServices"], + Commerce: files["commerce"], + CoreFP: files["CoreFP"], + CoreFPICXS: files["CoreFP.icxs"], } } func bundleFiles(bundle Bundle) map[string][]byte { return map[string][]byte{ - "CommerceKit": bundle.CommerceKit, - "CommerceCore": bundle.CommerceCore, - "CoreFP": bundle.CoreFP, - "CoreFP.icxs": bundle.CoreFPICXS, + "AppleMediaServices": bundle.AppleMediaServices, + "commerce": bundle.Commerce, + "CoreFP": bundle.CoreFP, + "CoreFP.icxs": bundle.CoreFPICXS, } } @@ -370,17 +261,3 @@ func mustDigest(value string) [32]byte { return digest } - -type contextTransport struct { - ctx context.Context - next http.RoundTripper -} - -func (t contextTransport) RoundTrip(request *http.Request) (*http.Response, error) { - response, err := t.next.RoundTrip(request.Clone(t.ctx)) - if err != nil { - return nil, fmt.Errorf("fetch Apple software update: %w", err) - } - - return response, nil -} diff --git a/internal/sap/assets/dyld.go b/internal/sap/assets/dyld.go new file mode 100644 index 00000000..c927359f --- /dev/null +++ b/internal/sap/assets/dyld.go @@ -0,0 +1,685 @@ +//nolint:wsl // Binary format parsing is clearer when related bounds checks remain adjacent. +package assets + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "os" + "strings" + + "github.com/blacktop/go-macho" + "github.com/blacktop/go-macho/pkg/fixupchains" + machotypes "github.com/blacktop/go-macho/types" +) + +const ( + dyldHeaderSize = 568 + dyldMappingSize = 56 + dyldImageInfoSize = 32 + dyldSubcacheEntrySize = 56 + dyldMaximumMappings = 64 + dyldMaximumImages = 100_000 + dyldMaximumSubcaches = 128 + dyldMaximumPathLength = 4096 + dyldSlideInfo5Size = 24 + dyldMachHeader64Size = 32 + dyldMachSegment64Command = 0x19 + dyldMaximumLoadCommandsSize = 16 << 20 + dyldMaximumSlideChainPointers = 1 << 16 +) + +type dyldCache struct { + members []*dyldCacheMember + primary *dyldCacheMember + slide5 *dyldSlideInfo5 +} + +type dyldCacheMember struct { + file io.ReaderAt + close func() error + size int64 + uuid [16]byte + mappings []dyldCacheMapping +} + +type dyldCacheMapping struct { + member *dyldCacheMember + address uint64 + size uint64 + fileOffset uint64 + slideInfoOffset uint64 + slideInfoSize uint64 +} + +type dyldSlideInfo5 struct { + pageSize uint32 + pageStartsCount uint32 + valueAdd uint64 +} + +type dyldCacheFile struct { + reader io.ReaderAt + size int64 + close func() error +} + +type dyldCacheOpener func(suffix string) (*dyldCacheFile, error) + +type dyldImage struct { + member *dyldCacheMember + address uint64 + offset uint64 +} + +type dyldRebase struct { + address uint64 + target uint64 +} + +func extractDyldImageFromFiles(opener dyldCacheOpener, imagePath, outputPath string) error { + cache, err := openDyldCache(opener) + if err != nil { + return fmt.Errorf("open shared cache: %w", err) + } + defer cache.Close() + + image, err := cache.image(imagePath) + if err != nil { + return fmt.Errorf("find %s in shared cache: %w", imagePath, err) + } + imageMachO, err := cache.openImage(image) + if err != nil { + return fmt.Errorf("read %s from shared cache: %w", imagePath, err) + } + defer imageMachO.Close() + + var chainedFixups *fixupchains.DyldChainedFixups + if imageMachO.HasFixups() { + chainedFixups, err = imageMachO.DyldChainedFixups() + if err != nil { + return fmt.Errorf("read %s chained fixups: %w", imagePath, err) + } + } + if err := imageMachO.Export(outputPath, chainedFixups, imageMachO.GetBaseAddress(), nil); err != nil { + return fmt.Errorf("export %s from shared cache: %w", imagePath, err) + } + + if err := cache.applySlide(outputPath); err != nil { + return fmt.Errorf("apply %s shared-cache pointers: %w", imagePath, err) + } + + return nil +} + +func (c *dyldCache) openImage(image *dyldImage) (*macho.File, error) { + linkeditAddress, err := image.linkeditAddress() + if err != nil { + return nil, err + } + linkedit, err := c.mappingForAddress(linkeditAddress) + if err != nil { + return nil, err + } + + reader := &dyldCacheReader{cache: c, member: linkedit.member, offset: 0} + converter := machotypes.VMAddrConverter{ + Converter: c.slidePointer, + VMAddr2Offet: func(address uint64) (uint64, error) { + mapping, err := c.mappingForAddress(address) + if err != nil { + return 0, err + } + + return mapping.fileOffset + address - mapping.address, nil + }, + Offet2VMAddr: func(offset uint64) (uint64, error) { + return c.addressForOffset(image.member, offset) + }, + } + sectionReader := machotypes.NewCustomSectionReader(image.member.file, &converter, 0, image.member.size) + + file, err := macho.NewFile( + io.NewSectionReader(image.member.file, int64(image.offset), image.member.size-int64(image.offset)), + macho.FileConfig{ + Offset: int64(image.offset), + SectionReader: sectionReader, + CacheReader: reader, + VMAddrConverter: converter, + }, + ) + if err != nil { + return nil, fmt.Errorf("parse shared-cache image: %w", err) + } + + return file, nil +} + +func openDyldCache(opener dyldCacheOpener) (*dyldCache, error) { + primary, header, err := openDyldCacheMember(opener, "") + if err != nil { + return nil, err + } + cache := &dyldCache{members: []*dyldCacheMember{primary}, primary: primary} + complete := false + + defer func() { + if !complete { + _ = cache.Close() + } + }() + + subcacheOffset := uint64(binary.LittleEndian.Uint32(header[392:])) + subcacheCount := uint64(binary.LittleEndian.Uint32(header[396:])) + if subcacheCount > dyldMaximumSubcaches || subcacheCount > 0 && !validDyldRange(primary.size, subcacheOffset, subcacheCount*dyldSubcacheEntrySize) { + return nil, fmt.Errorf("shared cache has an invalid subcache table") + } + entries := make([]byte, subcacheCount*dyldSubcacheEntrySize) + if len(entries) > 0 { + if _, err := primary.file.ReadAt(entries, int64(subcacheOffset)); err != nil { + return nil, fmt.Errorf("read shared-cache subcache table: %w", err) + } + } + for index := uint64(0); index < subcacheCount; index++ { + entry := entries[index*dyldSubcacheEntrySize : (index+1)*dyldSubcacheEntrySize] + suffix := strings.TrimRight(string(entry[24:56]), "\x00") + if suffix == "" || strings.ContainsAny(suffix, `/\\`) { + return nil, fmt.Errorf("shared cache has an invalid subcache suffix") + } + member, _, err := openDyldCacheMember(opener, suffix) + if err != nil { + return nil, fmt.Errorf("open shared-cache member %s: %w", suffix, err) + } + if !bytes.Equal(member.uuid[:], entry[:16]) { + _ = member.close() + + return nil, fmt.Errorf("shared-cache member %s has an unexpected UUID", suffix) + } + cache.members = append(cache.members, member) + } + + for _, member := range cache.members { + for _, mapping := range member.mappings { + if mapping.slideInfoSize == 0 { + continue + } + version, err := readDyldSlideVersion(mapping) + if err != nil { + return nil, err + } + if version != 5 { + return nil, fmt.Errorf("unsupported shared-cache slide version %d", version) + } + slide, err := readDyldSlideInfo5(mapping) + if err != nil { + return nil, err + } + if cache.slide5 == nil { + cache.slide5 = slide + } else if cache.slide5.pageSize != slide.pageSize || cache.slide5.valueAdd != slide.valueAdd { + return nil, fmt.Errorf("shared-cache members use incompatible slide information") + } + } + } + if cache.slide5 == nil { + return nil, fmt.Errorf("shared cache has no slide information") + } + complete = true + + return cache, nil +} + +func openDyldCacheMember(opener dyldCacheOpener, suffix string) (*dyldCacheMember, []byte, error) { + opened, err := opener(suffix) + if err != nil { + return nil, nil, err + } + fail := func(err error) (*dyldCacheMember, []byte, error) { + _ = opened.close() + + return nil, nil, err + } + if opened.size < dyldHeaderSize { + return fail(fmt.Errorf("shared-cache member is too small")) + } + header := make([]byte, dyldHeaderSize) + if _, err := opened.reader.ReadAt(header, 0); err != nil { + return fail(err) + } + magic := strings.TrimRight(string(header[:16]), "\x00") + if magic != "dyld_v1 arm64e" { + return fail(fmt.Errorf("unsupported shared-cache format %q", magic)) + } + + mappingOffset := uint64(binary.LittleEndian.Uint32(header[312:])) + mappingCount := uint64(binary.LittleEndian.Uint32(header[316:])) + if mappingCount == 0 || mappingCount > dyldMaximumMappings || !validDyldRange(opened.size, mappingOffset, mappingCount*dyldMappingSize) { + return fail(fmt.Errorf("shared-cache member has an invalid mapping table")) + } + mappingData := make([]byte, mappingCount*dyldMappingSize) + if _, err := opened.reader.ReadAt(mappingData, int64(mappingOffset)); err != nil { + return fail(err) + } + + member := &dyldCacheMember{file: opened.reader, close: opened.close, size: opened.size} + copy(member.uuid[:], header[88:104]) + + for index := uint64(0); index < mappingCount; index++ { + data := mappingData[index*dyldMappingSize : (index+1)*dyldMappingSize] + mapping := dyldCacheMapping{ + member: member, + address: binary.LittleEndian.Uint64(data[0:]), + size: binary.LittleEndian.Uint64(data[8:]), + fileOffset: binary.LittleEndian.Uint64(data[16:]), + slideInfoOffset: binary.LittleEndian.Uint64(data[24:]), + slideInfoSize: binary.LittleEndian.Uint64(data[32:]), + } + if mapping.size == 0 || mapping.address+mapping.size < mapping.address || !validDyldRange(opened.size, mapping.fileOffset, mapping.size) || mapping.slideInfoSize != 0 && !validDyldRange(opened.size, mapping.slideInfoOffset, mapping.slideInfoSize) { + return fail(fmt.Errorf("shared-cache member has an invalid mapping")) + } + + member.mappings = append(member.mappings, mapping) + } + + return member, header, nil +} + +func validDyldRange(size int64, offset, length uint64) bool { + return size >= 0 && offset <= uint64(size) && length <= uint64(size)-offset +} + +func (c *dyldCache) Close() error { + var first error + for _, member := range c.members { + if err := member.close(); err != nil && first == nil { + first = err + } + } + + return first +} + +func (c *dyldCache) image(path string) (*dyldImage, error) { + header := make([]byte, dyldHeaderSize) + if _, err := c.primary.file.ReadAt(header, 0); err != nil { + return nil, fmt.Errorf("read shared-cache header: %w", err) + } + imagesOffset := uint64(binary.LittleEndian.Uint32(header[448:])) + imagesCount := uint64(binary.LittleEndian.Uint32(header[452:])) + if imagesCount == 0 || imagesCount > dyldMaximumImages || !validDyldRange(c.primary.size, imagesOffset, imagesCount*dyldImageInfoSize) { + return nil, fmt.Errorf("shared cache has an invalid image table") + } + images := make([]byte, imagesCount*dyldImageInfoSize) + if _, err := c.primary.file.ReadAt(images, int64(imagesOffset)); err != nil { + return nil, fmt.Errorf("read shared-cache image table: %w", err) + } + for index := uint64(0); index < imagesCount; index++ { + data := images[index*dyldImageInfoSize : (index+1)*dyldImageInfoSize] + address := binary.LittleEndian.Uint64(data) + pathOffset := uint64(binary.LittleEndian.Uint32(data[24:])) + name, err := readDyldCString(c.primary.file, c.primary.size, pathOffset) + if err != nil { + return nil, err + } + if name != path { + continue + } + mapping, err := c.mappingForAddress(address) + if err != nil { + return nil, err + } + + return &dyldImage{ + member: mapping.member, + address: address, + offset: mapping.fileOffset + address - mapping.address, + }, nil + } + + return nil, fmt.Errorf("image was not found") +} + +func readDyldCString(file io.ReaderAt, size int64, offset uint64) (string, error) { + if !validDyldRange(size, offset, 1) { + return "", fmt.Errorf("shared cache contains an invalid string offset") + } + limit := min(uint64(dyldMaximumPathLength), uint64(size)-offset) + data := make([]byte, limit) + n, err := file.ReadAt(data, int64(offset)) + if err != nil && err != io.EOF { + return "", fmt.Errorf("read shared-cache string: %w", err) + } + data = data[:n] + if end := bytes.IndexByte(data, 0); end >= 0 { + return string(data[:end]), nil + } + + return "", fmt.Errorf("shared-cache string is not terminated") +} + +func (c *dyldCache) mappingForAddress(address uint64) (*dyldCacheMapping, error) { + for _, member := range c.members { + for index := range member.mappings { + mapping := &member.mappings[index] + if mapping.address <= address && address < mapping.address+mapping.size { + return mapping, nil + } + } + } + + return nil, fmt.Errorf("address %#x is outside the shared cache", address) +} + +func (c *dyldCache) addressForOffset(member *dyldCacheMember, offset uint64) (uint64, error) { + for _, mapping := range member.mappings { + if mapping.fileOffset <= offset && offset < mapping.fileOffset+mapping.size { + return mapping.address + offset - mapping.fileOffset, nil + } + } + + return 0, fmt.Errorf("offset %#x is outside the shared-cache member", offset) +} + +func (c *dyldCache) slidePointer(pointer uint64) uint64 { + if pointer == 0 { + return 0 + } + if _, err := c.mappingForAddress(pointer); err == nil { + return pointer + } + + return c.slide5.decode(pointer) +} + +func (i *dyldImage) linkeditAddress() (uint64, error) { + header := make([]byte, dyldMachHeader64Size) + if _, err := i.member.file.ReadAt(header, int64(i.offset)); err != nil { + return 0, fmt.Errorf("read Mach-O header: %w", err) + } + if binary.LittleEndian.Uint32(header) != uint32(machotypes.Magic64) { + return 0, fmt.Errorf("image has an invalid Mach-O magic") + } + commandCount := binary.LittleEndian.Uint32(header[16:]) + commandsSize := uint64(binary.LittleEndian.Uint32(header[20:])) + if commandsSize > dyldMaximumLoadCommandsSize || !validDyldRange(i.member.size, i.offset+dyldMachHeader64Size, commandsSize) { + return 0, fmt.Errorf("image has invalid load commands") + } + commands := make([]byte, commandsSize) + if _, err := i.member.file.ReadAt(commands, int64(i.offset+dyldMachHeader64Size)); err != nil { + return 0, fmt.Errorf("read Mach-O load commands: %w", err) + } + for index, offset := uint32(0), uint64(0); index < commandCount; index++ { + if offset+8 > uint64(len(commands)) { + return 0, fmt.Errorf("image load commands are truncated") + } + commandSize := uint64(binary.LittleEndian.Uint32(commands[offset+4:])) + if commandSize < 8 || offset+commandSize > uint64(len(commands)) { + return 0, fmt.Errorf("image has an invalid load command") + } + if binary.LittleEndian.Uint32(commands[offset:]) == dyldMachSegment64Command && commandSize >= 72 { + name := strings.TrimRight(string(commands[offset+8:offset+24]), "\x00") + if name == "__LINKEDIT" { + return binary.LittleEndian.Uint64(commands[offset+24:]), nil + } + } + offset += commandSize + } + + return 0, fmt.Errorf("image has no __LINKEDIT segment") +} + +type dyldCacheReader struct { + cache *dyldCache + member *dyldCacheMember + offset int64 +} + +func (r *dyldCacheReader) Read(data []byte) (int, error) { + n, err := r.member.file.ReadAt(data, r.offset) + r.offset += int64(n) + + return n, err //nolint:wrapcheck // Reader adapters must preserve io.EOF. +} + +func (r *dyldCacheReader) Seek(offset int64, whence int) (int64, error) { + switch whence { + case io.SeekStart: + case io.SeekCurrent: + offset += r.offset + case io.SeekEnd: + offset += r.member.size + default: + return 0, fmt.Errorf("invalid seek mode") + } + if offset < 0 { + return 0, fmt.Errorf("negative shared-cache offset") + } + r.offset = offset + + return offset, nil +} + +func (r *dyldCacheReader) ReadAt(data []byte, offset int64) (int, error) { + return r.member.file.ReadAt(data, offset) //nolint:wrapcheck // ReaderAt implementations must preserve io.EOF. +} + +func (r *dyldCacheReader) SeekToAddr(address uint64) error { + mapping, err := r.cache.mappingForAddress(address) + if err != nil { + return err + } + r.member = mapping.member + r.offset = int64(mapping.fileOffset + address - mapping.address) + + return nil +} + +func (r *dyldCacheReader) ReadAtAddr(data []byte, address uint64) (int, error) { + written := 0 + for written < len(data) { + mapping, err := r.cache.mappingForAddress(address) + if err != nil { + return written, err + } + available := mapping.address + mapping.size - address + count := min(uint64(len(data)-written), available) + offset := mapping.fileOffset + address - mapping.address + n, err := mapping.member.file.ReadAt(data[written:written+int(count)], int64(offset)) + written += n + address += uint64(n) + if err != nil { + return written, err //nolint:wrapcheck // ReaderAt implementations must preserve io.EOF. + } + if n != int(count) { + return written, io.ErrUnexpectedEOF + } + } + + return written, nil +} + +func readDyldSlideVersion(mapping dyldCacheMapping) (uint32, error) { + if mapping.slideInfoSize < 4 { + return 0, fmt.Errorf("shared cache has truncated slide information") + } + var data [4]byte + if _, err := mapping.member.file.ReadAt(data[:], int64(mapping.slideInfoOffset)); err != nil { + return 0, fmt.Errorf("read shared-cache slide version: %w", err) + } + + return binary.LittleEndian.Uint32(data[:]), nil +} + +func readDyldSlideInfo5(mapping dyldCacheMapping) (*dyldSlideInfo5, error) { + if mapping.slideInfoSize < dyldSlideInfo5Size { + return nil, fmt.Errorf("shared cache has truncated slide information") + } + data := make([]byte, dyldSlideInfo5Size) + if _, err := mapping.member.file.ReadAt(data, int64(mapping.slideInfoOffset)); err != nil { + return nil, fmt.Errorf("read shared-cache slide information: %w", err) + } + if binary.LittleEndian.Uint32(data) != 5 { + return nil, fmt.Errorf("unsupported shared-cache slide version %d", binary.LittleEndian.Uint32(data)) + } + slide := &dyldSlideInfo5{ + pageSize: binary.LittleEndian.Uint32(data[4:]), + pageStartsCount: binary.LittleEndian.Uint32(data[8:]), + valueAdd: binary.LittleEndian.Uint64(data[16:]), + } + if slide.pageSize == 0 || uint64(dyldSlideInfo5Size)+uint64(slide.pageStartsCount)*2 > mapping.slideInfoSize { + return nil, fmt.Errorf("shared cache has invalid slide information") + } + + return slide, nil +} + +func (s *dyldSlideInfo5) decode(pointer uint64) uint64 { + if pointer == 0 { + return 0 + } + target := pointer & ((uint64(1) << 34) - 1) + if pointer>>63 == 0 { + target |= (pointer >> 34 & 0xff) << 56 + } + + return s.valueAdd + target +} + +func (c *dyldCache) applySlide(path string) error { + file, err := os.OpenFile(path, os.O_RDWR, 0o600) + if err != nil { + return fmt.Errorf("open exported image: %w", err) + } + defer file.Close() + + exported, err := macho.NewFile(file) + if err != nil { + return fmt.Errorf("parse exported image: %w", err) + } + defer exported.Close() + + for _, segment := range exported.Segments() { + mapping, err := c.mappingForAddress(segment.Addr) + if err != nil { + return err + } + if mapping.slideInfoOffset == 0 { + continue + } + pageSize, err := c.slidePageSize(*mapping) + if err != nil { + return err + } + start := (segment.Addr - mapping.address) / pageSize + end := (segment.Addr + segment.Memsz - mapping.address + pageSize) / pageSize + rebases, err := c.rebases(*mapping, start, end) + if err != nil { + return err + } + for _, rebase := range rebases { + offset, err := exported.GetOffset(rebase.address) + if err != nil { + continue + } + if _, err := file.Seek(int64(offset), io.SeekStart); err != nil { + return fmt.Errorf("seek to exported pointer %#x: %w", offset, err) + } + if err := binary.Write(file, binary.LittleEndian, rebase.target); err != nil { + return fmt.Errorf("write exported pointer %#x: %w", rebase.target, err) + } + } + } + + if err := file.Sync(); err != nil { + return fmt.Errorf("sync exported image: %w", err) + } + + return nil +} + +func (c *dyldCache) rebases(mapping dyldCacheMapping, startPage, endPage uint64) ([]dyldRebase, error) { + version, err := readDyldSlideVersion(mapping) + if err != nil { + return nil, err + } + if version != 5 { + return nil, fmt.Errorf("unsupported shared-cache slide version %d", version) + } + + slide, err := readDyldSlideInfo5(mapping) + if err != nil { + return nil, err + } + starts, err := readDyldSlideArray(mapping, dyldSlideInfo5Size, slide.pageStartsCount) + if err != nil { + return nil, err + } + if endPage == 0 || endPage > uint64(len(starts)) { + endPage = uint64(len(starts)) + } + if startPage > endPage { + return nil, fmt.Errorf("invalid shared-cache slide page range") + } + + var rebases []dyldRebase + + for page := startPage; page < endPage; page++ { + start := starts[page] + if start == 0xffff { + continue + } + pageFileOffset := mapping.fileOffset + page*uint64(slide.pageSize) + pageAddress := mapping.address + page*uint64(slide.pageSize) + offset := uint64(start) + for count := 0; count < dyldMaximumSlideChainPointers; count++ { + if offset+8 > uint64(slide.pageSize) { + return nil, fmt.Errorf("shared-cache slide chain leaves its page") + } + var data [8]byte + if _, err := mapping.member.file.ReadAt(data[:], int64(pageFileOffset+offset)); err != nil { + return nil, fmt.Errorf("read shared-cache slide pointer: %w", err) + } + pointer := binary.LittleEndian.Uint64(data[:]) + rebases = append(rebases, dyldRebase{ + address: pageAddress + offset, + target: slide.decode(pointer), + }) + next := pointer >> 52 & 0x7ff + if next == 0 { + break + } + offset += next * 8 + if count == dyldMaximumSlideChainPointers-1 { + return nil, fmt.Errorf("shared-cache slide chain is too long") + } + } + } + + return rebases, nil +} + +func (c *dyldCache) slidePageSize(mapping dyldCacheMapping) (uint64, error) { + slide, err := readDyldSlideInfo5(mapping) + if err != nil { + return 0, err + } + + return uint64(slide.pageSize), nil +} + +func readDyldSlideArray(mapping dyldCacheMapping, offset, count uint32) ([]uint16, error) { + if count == 0 { + return nil, nil + } + data := make([]byte, uint64(count)*2) + if _, err := mapping.member.file.ReadAt(data, int64(mapping.slideInfoOffset+uint64(offset))); err != nil { + return nil, fmt.Errorf("read shared-cache slide table: %w", err) + } + values := make([]uint16, count) + for index := range values { + values[index] = binary.LittleEndian.Uint16(data[index*2:]) + } + + return values, nil +} diff --git a/internal/sap/assets/http_range.go b/internal/sap/assets/http_range.go new file mode 100644 index 00000000..449c8176 --- /dev/null +++ b/internal/sap/assets/http_range.go @@ -0,0 +1,163 @@ +package assets + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "strings" +) + +const remoteReadAttempts = 3 + +type remoteFile struct { + ctx context.Context + client *http.Client + url string + size int64 +} + +//nolint:wsl // Request construction and validation are clearer in protocol order. +func openRemoteFile(ctx context.Context, client *http.Client, source string) (*remoteFile, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, source, nil) + if err != nil { + return nil, fmt.Errorf("create remote image request: %w", err) + } + + request.Header.Set("Range", "bytes=0-0") + response, err := client.Do(request) + + if err != nil { + return nil, fmt.Errorf("open remote image: %w", err) + } + + defer response.Body.Close() + + if response.StatusCode != http.StatusPartialContent { + return nil, fmt.Errorf("open remote image: server returned %s without byte-range support", response.Status) + } + + start, end, size, err := parseContentRange(response.Header.Get("Content-Range")) + if err != nil { + return nil, fmt.Errorf("open remote image: %w", err) + } + if start != 0 || end != 0 { + return nil, fmt.Errorf("open remote image: server returned unexpected byte range %d-%d", start, end) + } + + return &remoteFile{ctx: ctx, client: client, url: source, size: size}, nil +} + +//nolint:wsl // Parsing checks are clearer in wire-format order. +func parseContentRange(value string) (int64, int64, int64, error) { + unitAndRange, total, ok := strings.Cut(value, "/") + if !ok || total == "" || total == "*" { + return 0, 0, 0, fmt.Errorf("invalid Content-Range %q", value) + } + size, err := strconv.ParseInt(total, 10, 64) + if err != nil || size <= 0 { + return 0, 0, 0, fmt.Errorf("invalid Content-Range %q", value) + } + rangeValue, ok := strings.CutPrefix(unitAndRange, "bytes ") + if !ok { + return 0, 0, 0, fmt.Errorf("invalid Content-Range %q", value) + } + + startValue, endValue, ok := strings.Cut(rangeValue, "-") + if !ok { + return 0, 0, 0, fmt.Errorf("invalid Content-Range %q", value) + } + start, startErr := strconv.ParseInt(startValue, 10, 64) + end, endErr := strconv.ParseInt(endValue, 10, 64) + if startErr != nil || endErr != nil || start < 0 || end < start || end >= size { + return 0, 0, 0, fmt.Errorf("invalid Content-Range %q", value) + } + + return start, end, size, nil +} + +//nolint:wsl // Range request validation is clearer in protocol order. +func (r *remoteFile) ReadAt(data []byte, offset int64) (int, error) { + if len(data) == 0 { + return 0, nil + } + + if offset < 0 { + return 0, errors.New("negative remote image offset") + } + + if offset >= r.size { + return 0, io.EOF + } + + wanted := len(data) + if remaining := r.size - offset; int64(wanted) > remaining { + wanted = int(remaining) + } + + total := 0 + + var lastErr error + + for attempt := 0; attempt < remoteReadAttempts && total < wanted; attempt++ { + start := offset + int64(total) + end := offset + int64(wanted) - 1 + + request, err := http.NewRequestWithContext(r.ctx, http.MethodGet, r.url, nil) + if err != nil { + return total, fmt.Errorf("create remote range request: %w", err) + } + + request.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end)) + response, err := r.client.Do(request) + if err != nil { + lastErr = err + + continue + } + + if response.StatusCode != http.StatusPartialContent { + response.Body.Close() + + return total, fmt.Errorf("read remote image range: server returned %s", response.Status) + } + actualStart, actualEnd, actualSize, err := parseContentRange(response.Header.Get("Content-Range")) + + if err != nil || actualStart != start || actualEnd != end || actualSize != r.size { + response.Body.Close() + if err != nil { + return total, fmt.Errorf("read remote image range: %w", err) + } + + return total, fmt.Errorf("read remote image range: server returned bytes %d-%d/%d, expected %d-%d/%d", actualStart, actualEnd, actualSize, start, end, r.size) + } + + count, readErr := io.ReadFull(response.Body, data[total:wanted]) + closeErr := response.Body.Close() + total += count + + if readErr == nil { + lastErr = closeErr + + break + } + + lastErr = readErr + } + + if total != wanted { + if lastErr == nil { + lastErr = io.ErrUnexpectedEOF + } + + return total, fmt.Errorf("read remote image at offset %d: %w", offset+int64(total), lastErr) + } + + if wanted != len(data) { + return total, io.EOF + } + + return total, nil +} diff --git a/internal/sap/assets/remote.go b/internal/sap/assets/remote.go new file mode 100644 index 00000000..063fa0bc --- /dev/null +++ b/internal/sap/assets/remote.go @@ -0,0 +1,170 @@ +//nolint:wsl // Archive extraction is clearer when resource setup remains adjacent. +package assets + +import ( + "archive/zip" + "context" + "fmt" + "io" + "net/http" + "os" + "path/filepath" +) + +const ( + appleMediaServicesImage = "/System/Library/PrivateFrameworks/AppleMediaServices.framework/Versions/A/AppleMediaServices" + fileSystemImageName = "043-70867-640.dmg.aea" + systemOSImageName = "043-70701-651.dmg.aea" + fileSystemArchiveSize = 11274289152 + fileSystemImageSize = 14189330432 + systemOSArchiveSize = 2415919104 + systemOSImageSize = 2879389696 + arm64CachePath = "/System/Library/dyld/dyld_shared_cache_arm64e" +) + +var frameworkImagePaths = []string{ + "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/commerce", + "/System/Library/PrivateFrameworks/CoreFP.framework/Versions/A/CoreFP", + "/System/Library/PrivateFrameworks/CoreFP.framework/Versions/A/Resources/CoreFP.icxs", +} + +func resolveRemoteImage(ctx context.Context) (Bundle, error) { + client := &http.Client{} + remote, err := openRemoteFile(ctx, client, macOSRestoreURL) + if err != nil { + return Bundle{}, fmt.Errorf("resolve macOS %s (%s) frameworks: %w", macOSVersion, macOSBuild, err) + } + + archive, err := zip.NewReader(remote, remote.size) + if err != nil { + return Bundle{}, fmt.Errorf("read macOS %s (%s) restore image: %w", macOSVersion, macOSBuild, err) + } + + fileSystemEntry, err := pinnedZipEntry(archive, fileSystemImageName, fileSystemArchiveSize, fileSystemArchiveSize) + if err != nil { + return Bundle{}, err + } + + systemOSEntry, err := pinnedZipEntry(archive, systemOSImageName, systemOSArchiveSize, systemOSArchiveSize) + if err != nil { + return Bundle{}, err + } + + workspace, err := os.MkdirTemp("", "ipatool-macos-27-*") + if err != nil { + return Bundle{}, fmt.Errorf("create macOS image workspace: %w", err) + } + defer os.RemoveAll(workspace) + + frameworkDirectory := filepath.Join(workspace, "frameworks") + if err := extractFrameworkFiles(ctx, client, remote, fileSystemEntry, frameworkDirectory); err != nil { + return Bundle{}, err + } + + appleMediaServicesPath := filepath.Join(frameworkDirectory, "AppleMediaServices") + if err := extractAppleMediaServices(ctx, client, remote, systemOSEntry, appleMediaServicesPath); err != nil { + return Bundle{}, fmt.Errorf("extract AppleMediaServices from macOS %s (%s): %w", macOSVersion, macOSBuild, err) + } + + files := make(map[string][]byte, len(requiredFiles)) + + for _, spec := range requiredFiles { + data, err := os.ReadFile(filepath.Join(frameworkDirectory, spec.name)) + if err != nil { + return Bundle{}, fmt.Errorf("read extracted %s: %w", spec.name, err) + } + + files[spec.name] = data + } + + bundle := bundleFrom(files) + if err := validate(bundle); err != nil { + return Bundle{}, err + } + + return bundle, nil +} + +func extractAppleMediaServices(ctx context.Context, client *http.Client, remote *remoteFile, entry *zip.File, destination string) error { + if err := ctx.Err(); err != nil { + return fmt.Errorf("extract AppleMediaServices: %w", err) + } + + offset, err := entry.DataOffset() + if err != nil { + return fmt.Errorf("locate encrypted SystemOS cryptex: %w", err) + } + encryptedImage := io.NewSectionReader(remote, offset, int64(entry.CompressedSize64)) + image, err := newAEAReader(ctx, client, encryptedImage, int64(entry.CompressedSize64)) + if err != nil { + return fmt.Errorf("open encrypted SystemOS cryptex: %w", err) + } + + if image.size != systemOSImageSize { + return fmt.Errorf("SystemOS cryptex has size %d, expected %d", image.size, systemOSImageSize) + } + + extractor, closeExtractor, err := newAPFSExtractor(image, image.size) + if err != nil { + return err + } + + defer func() { _ = closeExtractor() }() + + opener := func(suffix string) (*dyldCacheFile, error) { + file, err := extractor.openFile(ctx, arm64CachePath+suffix) + if err != nil { + return nil, fmt.Errorf("open arm64e shared-cache member %q: %w", suffix, err) + } + + return &dyldCacheFile{reader: file, size: file.size, close: func() error { return nil }}, nil + } + if err := extractDyldImageFromFiles(opener, appleMediaServicesImage, destination); err != nil { + return fmt.Errorf("export AppleMediaServices from arm64e shared cache: %w", err) + } + + return nil +} + +func pinnedZipEntry(archive *zip.Reader, name string, compressedSize, uncompressedSize uint64) (*zip.File, error) { + for _, entry := range archive.File { + if entry.Name != name { + continue + } + + if entry.Method != zip.Store || entry.CompressedSize64 != compressedSize || entry.UncompressedSize64 != uncompressedSize { + return nil, fmt.Errorf("macOS %s (%s) restore member %s does not match the pinned image", macOSVersion, macOSBuild, name) + } + + return entry, nil + } + + return nil, fmt.Errorf("macOS %s (%s) restore image does not contain %s", macOSVersion, macOSBuild, name) +} + +func extractFrameworkFiles(ctx context.Context, client *http.Client, remote *remoteFile, entry *zip.File, destination string) error { + if err := ctx.Err(); err != nil { + return fmt.Errorf("extract framework files: %w", err) + } + + offset, err := entry.DataOffset() + if err != nil { + return fmt.Errorf("locate encrypted macOS filesystem: %w", err) + } + + encryptedImage := io.NewSectionReader(remote, offset, int64(entry.CompressedSize64)) + image, err := newAEAReader(ctx, client, encryptedImage, int64(entry.CompressedSize64)) + if err != nil { + return fmt.Errorf("open encrypted macOS filesystem: %w", err) + } + + if image.size != fileSystemImageSize { + return fmt.Errorf("macOS filesystem has size %d, expected %d", image.size, fileSystemImageSize) + } + + if err := extractAPFSFiles(ctx, image, image.size, frameworkImagePaths, destination); err != nil { + return fmt.Errorf("extract frameworks from macOS %s (%s): %w", macOSVersion, macOSBuild, err) + } + + return nil +} diff --git a/internal/sap/assets/remote_integration_test.go b/internal/sap/assets/remote_integration_test.go new file mode 100644 index 00000000..b6e56d77 --- /dev/null +++ b/internal/sap/assets/remote_integration_test.go @@ -0,0 +1,68 @@ +package assets + +import ( + "archive/zip" + "context" + "crypto/sha256" + "net/http" + "os" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("macOS 27 remote framework extraction", Label("integration"), func() { + It("reads the pinned framework files without an extraction executable", func() { + if os.Getenv("IPATOOL_INTEGRATION_TESTS") == "" { + Skip("set IPATOOL_INTEGRATION_TESTS to run") + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + DeferCleanup(cancel) + + client := &http.Client{} + remote, err := openRemoteFile(ctx, client, macOSRestoreURL) + Expect(err).NotTo(HaveOccurred()) + + archive, err := zip.NewReader(remote, remote.size) + Expect(err).NotTo(HaveOccurred()) + entry, err := pinnedZipEntry(archive, fileSystemImageName, fileSystemArchiveSize, fileSystemArchiveSize) + Expect(err).NotTo(HaveOccurred()) + destination := GinkgoT().TempDir() + Expect(extractFrameworkFiles(ctx, client, remote, entry, destination)).To(Succeed()) + + for _, spec := range requiredFiles[1:] { + data, err := os.ReadFile(filepath.Join(destination, spec.name)) + Expect(err).NotTo(HaveOccurred(), spec.name) + Expect(data).To(HaveLen(spec.size), spec.name) + Expect(sha256.Sum256(data)).To(Equal(spec.digest), spec.name) + } + }) + + It("reads AppleMediaServices from the pinned arm64e shared cache", func() { + if os.Getenv("IPATOOL_INTEGRATION_TESTS") == "" { + Skip("set IPATOOL_INTEGRATION_TESTS to run") + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + DeferCleanup(cancel) + + client := &http.Client{} + remote, err := openRemoteFile(ctx, client, macOSRestoreURL) + Expect(err).NotTo(HaveOccurred()) + archive, err := zip.NewReader(remote, remote.size) + Expect(err).NotTo(HaveOccurred()) + entry, err := pinnedZipEntry(archive, systemOSImageName, systemOSArchiveSize, systemOSArchiveSize) + Expect(err).NotTo(HaveOccurred()) + + workspace := GinkgoT().TempDir() + output := filepath.Join(workspace, "AppleMediaServices") + Expect(extractAppleMediaServices(ctx, client, remote, entry, output)).To(Succeed()) + data, err := os.ReadFile(output) + Expect(err).NotTo(HaveOccurred()) + Expect(data).To(HaveLen(requiredFiles[0].size)) + Expect(sha256.Sum256(data)).To(Equal(requiredFiles[0].digest)) + }) +}) diff --git a/internal/sap/assets/remote_test.go b/internal/sap/assets/remote_test.go new file mode 100644 index 00000000..c2971e85 --- /dev/null +++ b/internal/sap/assets/remote_test.go @@ -0,0 +1,58 @@ +package assets + +import ( + "bytes" + "encoding/binary" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("native remote image reader", func() { + DescribeTable("parses complete Content-Range values", + func(value string, expectedStart, expectedEnd, expectedSize int64) { + start, end, size, err := parseContentRange(value) + Expect(err).NotTo(HaveOccurred()) + Expect(start).To(Equal(expectedStart)) + Expect(end).To(Equal(expectedEnd)) + Expect(size).To(Equal(expectedSize)) + }, + Entry("single byte", "bytes 0-0/26637307067", int64(0), int64(0), int64(26637307067)), + Entry("middle range", "bytes 100-199/1000", int64(100), int64(199), int64(1000)), + ) + + DescribeTable("rejects incomplete Content-Range values", + func(value string) { + _, _, _, err := parseContentRange(value) + Expect(err).To(HaveOccurred()) + }, + Entry("missing total", "bytes 0-0/*"), + Entry("missing separator", "bytes 0-0"), + Entry("zero length", "bytes 0-0/0"), + Entry("wrong unit", "items 0-0/1"), + Entry("range beyond total", "bytes 0-1/1"), + Entry("backwards range", "bytes 2-1/3"), + ) + + It("parses bounded AEA metadata records", func() { + var data bytes.Buffer + for key, value := range map[string]string{"first": "value", "second": "another"} { + record := append(append([]byte(key), 0), value...) + Expect(binary.Write(&data, binary.LittleEndian, uint32(len(record)+4))).To(Succeed()) + _, err := data.Write(record) + Expect(err).NotTo(HaveOccurred()) + } + + metadata, err := parseAEAMetadata(data.Bytes()) + Expect(err).NotTo(HaveOccurred()) + Expect(metadata).To(HaveKeyWithValue("first", []byte("value"))) + Expect(metadata).To(HaveKeyWithValue("second", []byte("another"))) + }) + + It("rejects an AEA metadata record outside the authentication block", func() { + data := make([]byte, 8) + binary.LittleEndian.PutUint32(data, 32) + _, err := parseAEAMetadata(data) + Expect(err).To(MatchError(ContainSubstring("invalid AEA metadata record length"))) + }) +}) diff --git a/internal/sap/assets/storeagent.go b/internal/sap/assets/storeagent.go deleted file mode 100644 index c61b6ab0..00000000 --- a/internal/sap/assets/storeagent.go +++ /dev/null @@ -1,86 +0,0 @@ -package assets - -import ( - "context" - "crypto/sha256" - "fmt" - "os" - "path/filepath" -) - -const storeAgentCacheDirectory = "apple-storeagent-v1" - -var storeAgentSpec = fileSpec{ - name: "storeagent", - path: "./System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeagent", - size: 2580176, - digest: mustDigest("70ce036f9dbcbc04db9511ebd08de0dd3cbc35ccc9d44b089c90170cb5453c59"), -} - -// LoadStoreAgent loads the pinned StoreAgent image from its separate cache or Apple payload. -func LoadStoreAgent(ctx context.Context) ([]byte, error) { - directory, err := storeAgentDirectory() - if err != nil { - return nil, err - } - - path := filepath.Join(directory, storeAgentSpec.name) - if data, err := readStoreAgent(path); err == nil { - return data, nil - } - - files, err := downloadFiles(ctx, []fileSpec{storeAgentSpec}, "download Apple StoreAgent asset") - if err != nil { - return nil, err - } - - data := files[storeAgentSpec.name] - if err := VerifyStoreAgent(data); err != nil { - return nil, err - } - - if err := os.MkdirAll(directory, 0o700); err != nil { - return nil, fmt.Errorf("create StoreAgent asset cache: %w", err) - } - - if err := replaceFile(path, data); err != nil { - return nil, fmt.Errorf("cache Apple StoreAgent asset: %w", err) - } - - return data, nil -} - -func storeAgentDirectory() (string, error) { - root, err := os.UserCacheDir() - if err != nil { - return "", fmt.Errorf("find user cache directory: %w", err) - } - - return filepath.Join(root, "ipatool", "sap", storeAgentCacheDirectory), nil -} - -func readStoreAgent(path string) ([]byte, error) { - data, err := readCacheFile(path, storeAgentSpec.size) - if err != nil { - return nil, fmt.Errorf("read cached Apple StoreAgent asset: %w", err) - } - - if err := VerifyStoreAgent(data); err != nil { - return nil, err - } - - return data, nil -} - -// VerifyStoreAgent verifies the image profile required by the StoreAgent runtime. -func VerifyStoreAgent(data []byte) error { - if len(data) != storeAgentSpec.size { - return fmt.Errorf("apple StoreAgent asset has size %d, expected %d", len(data), storeAgentSpec.size) - } - - if sha256.Sum256(data) != storeAgentSpec.digest { - return fmt.Errorf("apple StoreAgent asset failed integrity verification") - } - - return nil -} diff --git a/internal/sap/assets/storeagent_test.go b/internal/sap/assets/storeagent_test.go index 82168e5e..576f5328 100644 --- a/internal/sap/assets/storeagent_test.go +++ b/internal/sap/assets/storeagent_test.go @@ -1,32 +1,28 @@ package assets import ( - "bytes" - "crypto/sha256" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) -var _ = Describe("StoreAgent asset profile", func() { - It("pins the verified Apple payload asset", func() { - Expect(storeAgentSpec.name).To(Equal("storeagent")) - Expect(storeAgentSpec.path).To(Equal("./System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeagent")) - Expect(storeAgentSpec.size).To(Equal(2580176)) - Expect(storeAgentSpec.digest).To(Equal(mustDigest("70ce036f9dbcbc04db9511ebd08de0dd3cbc35ccc9d44b089c90170cb5453c59"))) - }) - - It("rejects an asset with the wrong size", func() { - Expect(VerifyStoreAgent(nil)).To(MatchError(ContainSubstring("has size 0, expected 2580176"))) +var _ = Describe("macOS 27 asset profile", func() { + It("pins the Apple restore image and cache namespace", func() { + Expect(macOSVersion).To(Equal("27.0.1")) + Expect(macOSBuild).To(Equal("26A434")) + Expect(macOSRestoreURL).To(Equal("https://updates.cdn-apple.com/2026FallFCS/59241290-5d51-4ca8-9df4-31624b9a4eac/UniversalMac_27.0.1_26A434_Restore.ipsw")) + Expect(assetCacheDirectory).To(ContainSubstring("macos-27-26A434")) }) - It("rejects an asset with the wrong digest", func() { - data := bytes.Repeat([]byte{0xa5}, storeAgentSpec.size) - Expect(sha256.Sum256(data)).NotTo(Equal(storeAgentSpec.digest)) - Expect(VerifyStoreAgent(data)).To(MatchError("apple StoreAgent asset failed integrity verification")) + It("pins every runtime artifact by size and digest", func() { + Expect(requiredFiles).To(ConsistOf( + fileSpec{name: "AppleMediaServices", size: 14813888, digest: mustDigest("95f4558aa2c0ccd9f96ed479e336b630b46bf4b4e0baabc1c1ac8c7d55e21ab4")}, + fileSpec{name: "commerce", size: 8092640, digest: mustDigest("76de40a4a32f691947cb926ce3b128d2fc446f7cac9755f831c406cf5a316ac9")}, + fileSpec{name: "CoreFP", size: 94842464, digest: mustDigest("2c30fa2b5f695fd66c0360dce72f54757d03ddb7107549715c12cb359a003b46")}, + fileSpec{name: "CoreFP.icxs", size: 7365344, digest: mustDigest("cb8f55330ec567da3e692dab5f5388bed0312dd9093340477c5759078cc2aa7b")}, + )) }) - It("uses a cache namespace separate from ordinary SAP assets", func() { - Expect(storeAgentCacheDirectory).NotTo(Equal("apple-assets-v2")) + It("rejects an incomplete bundle", func() { + Expect(validate(Bundle{})).To(MatchError(ContainSubstring("AppleMediaServices has size 0"))) }) }) diff --git a/internal/sap/cpio/reader.go b/internal/sap/cpio/reader.go deleted file mode 100644 index e322ecb6..00000000 --- a/internal/sap/cpio/reader.go +++ /dev/null @@ -1,86 +0,0 @@ -// Package cpio reads the old portable ASCII CPIO format used by Apple's -// software-update payload. It streams entry bodies so unrelated files are not -// retained while locating the small set needed by the SAP emulator. -package cpio - -import ( - "bytes" - "errors" - "fmt" - "io" - "strconv" -) - -const ( - headerSize = 76 - nameSizeOffset = 59 - fileSizeOffset = 65 -) - -var ( - headerMagic = []byte("070707") - trailerName = []byte("TRAILER!!!") -) - -type Reader struct { - source io.Reader - current *io.LimitedReader -} - -func NewReader(source io.Reader) *Reader { - return &Reader{source: source} -} - -func (r *Reader) Next() (string, io.Reader, error) { - if r.current != nil { - if _, err := io.Copy(io.Discard, r.current); err != nil { - return "", nil, fmt.Errorf("skip CPIO entry: %w", err) - } - } - - header := make([]byte, headerSize) - if _, err := io.ReadFull(r.source, header); err != nil { - return "", nil, fmt.Errorf("read CPIO header: %w", err) - } - - if !bytes.Equal(header[:len(headerMagic)], headerMagic) { - return "", nil, fmt.Errorf("invalid CPIO magic %q", header[:len(headerMagic)]) - } - - nameSize, err := parseOctal(header[nameSizeOffset:fileSizeOffset]) - if err != nil || nameSize < 1 { - return "", nil, fmt.Errorf("invalid CPIO name size %q", header[nameSizeOffset:fileSizeOffset]) - } - - fileSize, err := parseOctal(header[fileSizeOffset:headerSize]) - if err != nil { - return "", nil, fmt.Errorf("invalid CPIO file size %q", header[fileSizeOffset:headerSize]) - } - - name := make([]byte, nameSize) - if _, err := io.ReadFull(r.source, name); err != nil { - return "", nil, fmt.Errorf("read CPIO name: %w", err) - } - - if name[len(name)-1] != 0 { - return "", nil, errors.New("CPIO name is not NUL-terminated") - } - - name = name[:len(name)-1] - if bytes.Equal(name, trailerName) { - return "", nil, io.EOF - } - - r.current = &io.LimitedReader{R: r.source, N: int64(fileSize)} - - return string(name), r.current, nil -} - -func parseOctal(value []byte) (uint64, error) { - parsed, err := strconv.ParseUint(string(value), 8, 64) - if err != nil { - return 0, fmt.Errorf("parse CPIO octal value: %w", err) - } - - return parsed, nil -} diff --git a/internal/sap/cpio/reader_test.go b/internal/sap/cpio/reader_test.go deleted file mode 100644 index 086d2a8b..00000000 --- a/internal/sap/cpio/reader_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package cpio - -import ( - "bytes" - "fmt" - "io" - "testing" -) - -func TestReaderStreamsEntries(t *testing.T) { - archive := append(cpioEntry("first", []byte("ignored")), cpioEntry("second", []byte("wanted"))...) - archive = append(archive, cpioEntry("TRAILER!!!", nil)...) - reader := NewReader(bytes.NewReader(archive)) - - name, _, err := reader.Next() - if err != nil { - t.Fatal(err) - } - - if name != "first" { - t.Fatalf("name = %q, want first", name) - } - - name, body, err := reader.Next() - if err != nil { - t.Fatal(err) - } - - data, err := io.ReadAll(body) - if err != nil { - t.Fatal(err) - } - - if name != "second" || string(data) != "wanted" { - t.Fatalf("entry = %q %q, want second wanted", name, data) - } - - if _, _, err := reader.Next(); err != io.EOF { - t.Fatalf("trailer error = %v, want EOF", err) - } -} - -func cpioEntry(name string, body []byte) []byte { - nameBytes := append([]byte(name), 0) - header := fmt.Sprintf( - "070707%06o%06o%06o%06o%06o%06o%06o%011o%06o%011o", - 0, 0, 0, 0, 0, 1, 0, 0, len(nameBytes), len(body), - ) - entry := append([]byte(header), nameBytes...) - - return append(entry, body...) -} From 8232b22b94819680aada52ac20641aaebe11bcb7 Mon Sep 17 00:00:00 2001 From: Majd Alfhaily Date: Sat, 10 Oct 2026 18:31:14 +0200 Subject: [PATCH 3/4] feat: add arm64 sap emulation --- internal/sap/machimage/image.go | 302 +++++++++++++++++++-- internal/sap/machine/arm64.go | 227 ++++++++++++++++ internal/sap/machine/machine.go | 196 ++++++++++--- internal/sap/machine/machine_test.go | 2 +- internal/sap/machine/macos27.go | 37 +++ internal/sap/machine/shim_memory.go | 100 +++++++ internal/sap/machine/shim_platform.go | 140 +++++++++- internal/sap/machine/shim_platform_test.go | 64 +++++ internal/sap/machine/shims.go | 55 ++-- internal/sap/unicorn/engine.go | 75 +++-- internal/sap/unicorn/engine_test.go | 24 +- internal/sap/unicorn/hook_test.go | 14 +- 12 files changed, 1098 insertions(+), 138 deletions(-) create mode 100644 internal/sap/machine/arm64.go create mode 100644 internal/sap/machine/macos27.go create mode 100644 internal/sap/machine/shim_platform_test.go diff --git a/internal/sap/machimage/image.go b/internal/sap/machimage/image.go index 9c7c753c..5e0f16e8 100644 --- a/internal/sap/machimage/image.go +++ b/internal/sap/machimage/image.go @@ -6,8 +6,10 @@ import ( "errors" "fmt" "math" + "sort" "github.com/blacktop/go-macho" + "github.com/blacktop/go-macho/pkg/fixupchains" "github.com/blacktop/go-macho/types" ) @@ -38,12 +40,18 @@ type Image struct { segments []segment binds []types.Bind rebases []types.Rebase + chained *fixupchains.DyldChainedFixups relocated bool loadedBase uint64 } +func (i *Image) Base() uint64 { + return i.base +} + +//nolint:wsl // Image validation and fixup discovery are clearer in execution order. func Open(name string, input []byte) (*Image, error) { - data, err := amd64Slice(input) + data, err := arm64Slice(input) if err != nil { return nil, fmt.Errorf("open %s: %w", name, err) } @@ -53,8 +61,8 @@ func Open(name string, input []byte) (*Image, error) { return nil, fmt.Errorf("open %s Mach-O: %w", name, err) } - if file.CPU != types.CPUAmd64 { - return nil, fmt.Errorf("open %s: expected x86-64 Mach-O, found %s", name, file.CPU) + if file.CPU != types.CPUArm64 { + return nil, fmt.Errorf("open %s: expected arm64 Mach-O, found %s", name, file.CPU) } image := &Image{ @@ -76,40 +84,107 @@ func Open(name string, input []byte) (*Image, error) { if err := image.validateSegments(); err != nil { return nil, err } - - image.binds, err = file.GetBindInfo() - if err != nil { - return nil, fmt.Errorf("read %s bindings: %w", name, err) + if err := image.removePointerAuthentication(); err != nil { + return nil, err } - image.rebases, err = file.GetRebaseInfo() - if err != nil { - return nil, fmt.Errorf("read %s rebases: %w", name, err) + if file.HasDyldChainedFixups() { + image.chained, err = file.DyldChainedFixups() + if err != nil { + return nil, fmt.Errorf("read %s chained fixups: %w", name, err) + } + + if _, err := image.chained.Parse(); err != nil { + return nil, fmt.Errorf("parse %s chained fixups: %w", name, err) + } + } else { + image.binds, err = file.GetBindInfo() + if err != nil && !errors.Is(err, macho.ErrMachODyldInfoNotFound) { + return nil, fmt.Errorf("read %s bindings: %w", name, err) + } + + image.rebases, err = file.GetRebaseInfo() + if err != nil && !errors.Is(err, macho.ErrMachODyldInfoNotFound) { + return nil, fmt.Errorf("read %s rebases: %w", name, err) + } } return image, nil } -func amd64Slice(input []byte) ([]byte, error) { +//nolint:wsl // Instruction decoding is clearer when related operations stay together. +func (i *Image) removePointerAuthentication() error { + const nop = uint32(0xd503201f) + + for _, section := range i.file.Sections { + if !section.Flags.IsPureInstructions() && !section.Flags.IsSomeInstructions() { + continue + } + start := uint64(section.Offset) + end, overflow := add(start, section.Size) + if overflow || end > uint64(len(i.data)) { + return fmt.Errorf("instruction section %s exceeds %s", section.Name, i.name) + } + for offset := start; offset+4 <= end; offset += 4 { + instruction := binary.LittleEndian.Uint32(i.data[offset:]) + var replacement uint32 + + switch instruction & 0xfffffc00 { + case 0xd71f0800, 0xd71f0c00, 0xd61f0800, 0xd61f0c00: + replacement = 0xd61f0000 | (instruction & 0x3e0) // br xN + case 0xd73f0800, 0xd73f0c00, 0xd63f0800, 0xd63f0c00: + replacement = 0xd63f0000 | (instruction & 0x3e0) // blr xN + case 0xdac10000, 0xdac10400, 0xdac10800, 0xdac10c00, + 0xdac11000, 0xdac11400, 0xdac11800, 0xdac11c00: + replacement = nop + default: + switch instruction { + case 0xd65f0bff, 0xd65f0fff: + replacement = 0xd65f03c0 // ret + case 0xd503211f, 0xd503215f, 0xd503219f, 0xd50321df, + 0xd503233f, 0xd503237f, 0xd50323bf, 0xd50323ff, + 0xd50320ff: + replacement = nop + default: + opcode := instruction & 0xffffffe0 + if opcode == 0xdac123e0 || opcode == 0xdac127e0 || + opcode == 0xdac12be0 || opcode == 0xdac12fe0 || + opcode == 0xdac133e0 || opcode == 0xdac137e0 || + opcode == 0xdac13be0 || opcode == 0xdac13fe0 || + opcode == 0xdac143e0 || opcode == 0xdac147e0 { + replacement = nop + } + } + } + if replacement != 0 { + binary.LittleEndian.PutUint32(i.data[offset:], replacement) + } + } + } + + return nil +} + +func arm64Slice(input []byte) ([]byte, error) { fat, err := macho.NewFatFile(bytes.NewReader(input)) if err != nil { return bytes.Clone(input), nil } for _, architecture := range fat.Arches { - if architecture.CPU != types.CPUAmd64 { + if architecture.CPU != types.CPUArm64 || architecture.SubCPU&types.CpuSubtypeMask != types.CPUSubtypeArm64E { continue } end, overflow := add(uint64(architecture.Offset), uint64(architecture.Size)) if overflow || end > uint64(len(input)) { - return nil, errors.New("x86-64 slice exceeds input size") + return nil, errors.New("arm64e slice exceeds input size") } return bytes.Clone(input[architecture.Offset:end]), nil } - return nil, errors.New("universal binary has no x86-64 slice") + return nil, errors.New("universal binary has no arm64e slice") } func (i *Image) Export(name string, loadBase uint64) (uint64, error) { @@ -185,18 +260,170 @@ func (i *Image) Relocate(loadBase uint64, resolve func(string) (uint64, error)) } } + if err := i.relocateChained(loadBase, resolve); err != nil { + return err + } + i.relocated = true i.loadedBase = loadBase return nil } +//nolint:wsl // Chained-fixup decoding is clearer when related operations stay together. +func (i *Image) relocateChained(loadBase uint64, resolve func(string) (uint64, error)) error { + if i.chained == nil { + return nil + } + + for _, start := range i.chained.Starts { + for _, fixup := range start.Fixups { + offset := fixup.Offset() + if offset > uint64(len(i.data)) || pointerSize > uint64(len(i.data))-offset { + return fmt.Errorf("chained fixup at %#x exceeds %s", offset, i.name) + } + + var address uint64 + + switch { + case fixup.IsRebase(): + var target uint64 + switch rebase := fixup.(type) { + case fixupchains.DyldChainedPtr64RebaseOffset: + // DYLD_CHAINED_PTR_64_OFFSET encodes an image-relative + // target. go-macho's Rebase currently subtracts the + // preferred address from that offset a second time. + target = rebase.UnpackedTarget() + case fixupchains.DyldChainedPtrArm64eRebase: + target = rebase.Target() + + if i.chained.PointerFormat == fixupchains.DYLD_CHAINED_PTR_ARM64E { + absolute := rebase.UnpackTarget() & 0x00ffffffffffffff + if absolute < i.base { + return fmt.Errorf("chained rebase at %#x precedes the base of %s", offset, i.name) + } + target = absolute - i.base + } + case fixupchains.DyldChainedPtrArm64eAuthRebase: + target = rebase.Target() + default: + var err error + + target, err = i.chained.Rebase(offset, i.base) + if err != nil { + return fmt.Errorf("decode chained rebase at %#x in %s: %w", offset, i.name, err) + } + } + + var overflow bool + + address, overflow = add(loadBase, target) + if overflow { + return fmt.Errorf("chained rebase at %#x with target %#x, image base %#x, load base %#x, format %s, and type %T overflows in %s", offset, target, i.base, loadBase, i.chained.PointerFormat, fixup, i.name) + } + case fixup.IsBind(): + binding, ok := fixup.(fixupchains.Bind) + if !ok { + return fmt.Errorf("invalid chained bind at %#x in %s", offset, i.name) + } + + var err error + + address, err = resolve(binding.Name()) + if err != nil { + return fmt.Errorf("resolve %s for %s: %w", binding.Name(), i.name, err) + } + + addend := int64(binding.Addend()) + switch arm64e := fixup.(type) { + case fixupchains.DyldChainedPtrArm64eBind: + addend = arm64e.SignExtendedAddend() + case fixupchains.DyldChainedPtrArm64eBind24: + addend = arm64e.SignExtendedAddend() + } + if ordinal := binding.Ordinal(); ordinal < uint64(len(i.chained.Imports)) { + addend += int64(i.chained.Imports[ordinal].Addend()) + } + + address, err = addSigned(address, addend) + if err != nil { + return fmt.Errorf("apply chained addend for %s in %s: %w", binding.Name(), i.name, err) + } + default: + return fmt.Errorf("unknown chained fixup at %#x in %s", offset, i.name) + } + + if err := i.putPointer(offset, address); err != nil { + return err + } + } + } + + return nil +} + +// PreparePrelinked prepares an image extracted from a dyld shared cache. Such +// images have already had their cache slide information applied, so they must +// remain at their preferred addresses. external maps cache symbol addresses to +// the import names implemented by the emulator. +func (i *Image) PreparePrelinked(external map[uint64]string, resolve func(string) (uint64, error)) error { + if i.relocated { + return fmt.Errorf("%s is already relocated", i.name) + } + + patched := make(map[uint64]uint64, len(external)) + + for address, name := range external { + value, err := resolve(name) + if err != nil { + return fmt.Errorf("resolve %s for %s: %w", name, i.name, err) + } + + patched[address] = value + } + + for _, item := range i.segments { + if item.name == "__TEXT" || item.name == "__LINKEDIT" || item.fileSize < pointerSize { + continue + } + + start := item.fileOff + if remainder := item.address % pointerSize; remainder != 0 { + start += pointerSize - remainder + } + + end := item.fileOff + item.fileSize + for offset := start; offset <= end-pointerSize; offset += pointerSize { + current := binary.LittleEndian.Uint64(i.data[offset : offset+pointerSize]) + + value, ok := patched[current] + if !ok { + continue + } + + if err := i.putPointer(offset, value); err != nil { + return err + } + } + } + + i.relocated = true + i.loadedBase = i.base + + return nil +} + func (i *Image) Load(memory Memory) error { if !i.relocated { return fmt.Errorf("%s must be relocated before loading", i.name) } - var span uint64 + type memoryRange struct { + start uint64 + end uint64 + } + + ranges := make([]memoryRange, 0, len(i.segments)) for _, item := range i.segments { if item.name == "__PAGEZERO" || item.size == 0 { @@ -207,25 +434,50 @@ func (i *Image) Load(memory Memory) error { return fmt.Errorf("segment %s in %s precedes image base", item.name, i.name) } - end, overflow := add(item.address-i.base, item.size) - if overflow || end > maxImageSpan { - return fmt.Errorf("segment %s makes %s too large", item.name, i.name) + address, overflow := add(i.loadedBase, item.address-i.base) + if overflow { + return fmt.Errorf("segment %s address overflows in %s", item.name, i.name) + } + + end, overflow := add(address, item.size) + if overflow { + return fmt.Errorf("segment %s range overflows in %s", item.name, i.name) } - span = max(span, end) + ranges = append(ranges, memoryRange{start: address - address%pageSize, end: align(end, pageSize)}) } - span = align(span, pageSize) - if span == 0 { + if len(ranges) == 0 { return fmt.Errorf("%s has no loadable segments", i.name) } - if _, overflow := add(i.loadedBase, span); overflow { - return fmt.Errorf("load range for %s overflows", i.name) + sort.Slice(ranges, func(left, right int) bool { return ranges[left].start < ranges[right].start }) + merged := ranges[:0] + + for _, current := range ranges { + last := len(merged) - 1 + if last >= 0 && current.start <= merged[last].end { + merged[last].end = max(merged[last].end, current.end) + + continue + } + + merged = append(merged, current) } - if err := memory.MemMap(i.loadedBase, span); err != nil { - return fmt.Errorf("map %s: %w", i.name, err) + var mapped uint64 + + for _, item := range merged { + size := item.end - item.start + + mapped, _ = add(mapped, size) + if mapped > maxImageSpan { + return fmt.Errorf("mapped segments make %s too large", i.name) + } + + if err := memory.MemMap(item.start, size); err != nil { + return fmt.Errorf("map %s: %w", i.name, err) + } } for _, item := range i.segments { diff --git a/internal/sap/machine/arm64.go b/internal/sap/machine/arm64.go new file mode 100644 index 00000000..e05d5345 --- /dev/null +++ b/internal/sap/machine/arm64.go @@ -0,0 +1,227 @@ +//nolint:wsl // Instruction decoding is clearer when related operations stay together. +package machine + +import ( + "encoding/binary" + "fmt" + + "github.com/majd/ipatool/v2/internal/sap/unicorn" +) + +const ( + arm64AtomicMask = uint32(0xffe0fc00) + arm64CASAL32 = uint32(0x88e0fc00) + arm64CASAL64 = uint32(0xc8e0fc00) + arm64SWPAL32 = uint32(0xb8e08000) + arm64SWPAL64 = uint32(0xf8e08000) + arm64EOR3Mask = uint32(0xffe08000) + arm64EOR3 = uint32(0xce000000) +) + +type arm64Compatibility struct { + engine *unicorn.Engine +} + +func newARM64Compatibility(engine *unicorn.Engine) *arm64Compatibility { + return &arm64Compatibility{engine: engine} +} + +func (c *arm64Compatibility) emulateInvalidInstruction() (bool, error) { + pc, err := c.engine.RegRead(unicorn.RegPC) + if err != nil { + return false, fmt.Errorf("read arm64 instruction address: %w", err) + } + + data, err := c.engine.MemRead(pc, 4) + if err != nil { + return false, nil + } + + instruction := binary.LittleEndian.Uint32(data) + if instruction&arm64EOR3Mask == arm64EOR3 { + if err := c.emulateEOR3(instruction); err != nil { + return false, err + } + if err := c.engine.RegWrite(unicorn.RegPC, pc+4); err != nil { + return false, fmt.Errorf("advance arm64 instruction address: %w", err) + } + + return true, nil + } + + opcode := instruction & arm64AtomicMask + + var width uint64 + + switch opcode { + case arm64CASAL32, arm64SWPAL32: + width = 4 + case arm64CASAL64, arm64SWPAL64: + width = 8 + default: + return false, nil + } + + source := (instruction >> 16) & 31 + base := (instruction >> 5) & 31 + destination := instruction & 31 + + address, err := c.readBaseRegister(base) + if err != nil { + return false, err + } + + memory, err := c.engine.MemRead(address, width) + if err != nil { + return false, fmt.Errorf("read arm64 atomic operand at %#x: %w", address, err) + } + + oldValue := binary.LittleEndian.Uint64(append(memory, make([]byte, 8-width)...)) + sourceValue, err := c.readValueRegister(source) + if err != nil { + return false, err + } + + if width == 4 { + sourceValue = uint64(uint32(sourceValue)) + } + + switch opcode { + case arm64CASAL32, arm64CASAL64: + newValue, readErr := c.readValueRegister(destination) + if readErr != nil { + return false, readErr + } + if width == 4 { + newValue = uint64(uint32(newValue)) + } + if oldValue == sourceValue { + if err := c.writeMemoryValue(address, newValue, width); err != nil { + return false, err + } + } + if err := c.writeValueRegister(source, oldValue); err != nil { + return false, err + } + case arm64SWPAL32, arm64SWPAL64: + if err := c.writeMemoryValue(address, sourceValue, width); err != nil { + return false, err + } + if err := c.writeValueRegister(destination, oldValue); err != nil { + return false, err + } + } + + if err := c.engine.RegWrite(unicorn.RegPC, pc+4); err != nil { + return false, fmt.Errorf("advance arm64 instruction address: %w", err) + } + + return true, nil +} + +func (c *arm64Compatibility) emulateEOR3(instruction uint32) error { + destination := instruction & 31 + first := (instruction >> 5) & 31 + third := (instruction >> 10) & 31 + second := (instruction >> 16) & 31 + + firstValue, err := c.engine.RegRead128(unicorn.RegV0 + int(first)) + if err != nil { + return fmt.Errorf("read arm64 vector register %d: %w", first, err) + } + secondValue, err := c.engine.RegRead128(unicorn.RegV0 + int(second)) + if err != nil { + return fmt.Errorf("read arm64 vector register %d: %w", second, err) + } + thirdValue, err := c.engine.RegRead128(unicorn.RegV0 + int(third)) + if err != nil { + return fmt.Errorf("read arm64 vector register %d: %w", third, err) + } + + var result [16]byte + for index := range result { + result[index] = firstValue[index] ^ secondValue[index] ^ thirdValue[index] + } + if err := c.engine.RegWrite128(unicorn.RegV0+int(destination), result); err != nil { + return fmt.Errorf("write arm64 vector register %d: %w", destination, err) + } + + return nil +} + +func (c *arm64Compatibility) readBaseRegister(index uint32) (uint64, error) { + register := unicorn.RegSP + + if index != 31 { + var ok bool + register, ok = arm64GeneralRegister(index) + if !ok { + return 0, fmt.Errorf("invalid arm64 base register %d", index) + } + } + + value, err := c.engine.RegRead(register) + if err != nil { + return 0, fmt.Errorf("read arm64 base register %d: %w", index, err) + } + + return value, nil +} + +func (c *arm64Compatibility) readValueRegister(index uint32) (uint64, error) { + if index == 31 { + return 0, nil + } + + register, ok := arm64GeneralRegister(index) + if !ok { + return 0, fmt.Errorf("invalid arm64 value register %d", index) + } + + value, err := c.engine.RegRead(register) + if err != nil { + return 0, fmt.Errorf("read arm64 value register %d: %w", index, err) + } + + return value, nil +} + +func (c *arm64Compatibility) writeValueRegister(index uint32, value uint64) error { + if index == 31 { + return nil + } + + register, ok := arm64GeneralRegister(index) + if !ok { + return fmt.Errorf("invalid arm64 value register %d", index) + } + if err := c.engine.RegWrite(register, value); err != nil { + return fmt.Errorf("write arm64 value register %d: %w", index, err) + } + + return nil +} + +func (c *arm64Compatibility) writeMemoryValue(address, value, width uint64) error { + var data [8]byte + + binary.LittleEndian.PutUint64(data[:], value) + if err := c.engine.MemWrite(address, data[:width]); err != nil { + return fmt.Errorf("write arm64 atomic operand at %#x: %w", address, err) + } + + return nil +} + +func arm64GeneralRegister(index uint32) (int, bool) { + switch { + case index <= 28: + return unicorn.RegX0 + int(index), true + case index == 29: + return unicorn.RegX29, true + case index == 30: + return unicorn.RegLR, true + default: + return 0, false + } +} diff --git a/internal/sap/machine/machine.go b/internal/sap/machine/machine.go index 4f986fa0..23ae2ea0 100644 --- a/internal/sap/machine/machine.go +++ b/internal/sap/machine/machine.go @@ -18,8 +18,6 @@ const sapGuestTimeout = time.Minute const ( returnAddress = uint64(0x0000000100000000) coreFPBase = uint64(0x0000100000000000) - commerceBase = uint64(0x0000100040000000) - kitBase = uint64(0x0000100080000000) scratchBase = uint64(0x0000300000000000) scratchSize = uint64(32 << 20) heapBase = uint64(0x0000400000000000) @@ -50,6 +48,7 @@ type entryPoints struct { type Machine struct { engine *unicorn.Engine + arm64 *arm64Compatibility services *shims entry entryPoints scratchCursor uint64 @@ -57,9 +56,12 @@ type Machine struct { } type imageSpec struct { - name string - data []byte - base uint64 + name string + data []byte + base uint64 + prelinked map[uint64]string + slots map[uint64]string + functions map[uint64]string } type runtimeOptions struct { @@ -93,11 +95,16 @@ func openRuntime(ctx context.Context, bundle assets.Bundle, options runtimeOptio return nil, nil, fmt.Errorf("open SAP runtime: %w", err) } - imageSpecs := make([]imageSpec, 0, 3+len(options.extraImages)) + imageSpecs := make([]imageSpec, 0, 2+len(options.extraImages)) imageSpecs = append(imageSpecs, imageSpec{name: "CoreFP", data: bundle.CoreFP, base: coreFPBase}, - imageSpec{name: "CommerceCore", data: bundle.CommerceCore, base: commerceBase}, - imageSpec{name: "CommerceKit", data: bundle.CommerceKit, base: kitBase}, + imageSpec{ + name: "AppleMediaServices", + data: bundle.AppleMediaServices, + prelinked: macOS27SharedCacheFunctions, + slots: macOS27SharedCacheSlots, + functions: macOS27SharedCacheFunctions, + }, ) imageSpecs = append(imageSpecs, options.extraImages...) @@ -126,16 +133,7 @@ func openRuntime(ctx context.Context, bundle assets.Bundle, options runtimeOptio coreExports[name] = address } - commerceCore := images["CommerceCore"] - macAddress, err := commerceCore.Export("_get_mac_address", commerceBase) - - if err != nil { - return nil, nil, fmt.Errorf("resolve CommerceCore MAC address export: %w", err) - } - - exports["_get_mac_address"] = macAddress - - commerceKit := images["CommerceKit"] + appleMediaServices := images["AppleMediaServices"] for _, name := range []string{ "_cp2g1b9ro", "_Mib5yocT", @@ -143,9 +141,9 @@ func openRuntime(ctx context.Context, bundle assets.Bundle, options runtimeOptio "_IPaI1oem5iL", "_jEHf8Xzsv8K", } { - address, err := commerceKit.Export(name, kitBase) + address, err := appleMediaServices.Export(name, appleMediaServices.Base()) if err != nil { - return nil, nil, fmt.Errorf("resolve CommerceKit export %s: %w", name, err) + return nil, nil, fmt.Errorf("resolve AppleMediaServices export %s: %w", name, err) } exports[name] = address @@ -160,7 +158,7 @@ func openRuntime(ctx context.Context, bundle assets.Bundle, options runtimeOptio return nil, nil, fmt.Errorf("create Unicorn engine: %w", err) } - machine := &Machine{engine: engine} + machine := &Machine{engine: engine, arm64: newARM64Compatibility(engine)} ready := false defer func() { @@ -183,7 +181,7 @@ func openRuntime(ctx context.Context, bundle assets.Bundle, options runtimeOptio } } - if err := engine.MemWrite(returnAddress, []byte{0xF4}); err != nil { + if err := engine.MemWrite(returnAddress, []byte{0x00, 0x00, 0x20, 0xd4}); err != nil { return nil, nil, fmt.Errorf("write SAP guest return instruction: %w", err) } @@ -200,9 +198,28 @@ func openRuntime(ctx context.Context, bundle assets.Bundle, options runtimeOptio return machine.services.resolve(name) } + for _, spec := range imageSpecs { + if err := loadPrelinkedSlots(engine, spec.slots, resolver); err != nil { + return nil, nil, fmt.Errorf("load %s shared-cache slots: %w", spec.name, err) + } + + if err := loadPrelinkedFunctions(engine, spec.functions, resolver); err != nil { + return nil, nil, fmt.Errorf("load %s shared-cache functions: %w", spec.name, err) + } + } + for _, spec := range imageSpecs { image := images[spec.name] - if err := image.Relocate(spec.base, resolver); err != nil { + + var err error + + if spec.prelinked != nil { + err = image.PreparePrelinked(spec.prelinked, resolver) + } else { + err = image.Relocate(spec.base, resolver) + } + + if err != nil { return nil, nil, fmt.Errorf("relocate SAP guest image: %w", err) } @@ -216,6 +233,69 @@ func openRuntime(ctx context.Context, bundle assets.Bundle, options runtimeOptio return machine, exports, nil } +func loadPrelinkedFunctions(engine *unicorn.Engine, functions map[uint64]string, resolve func(string) (uint64, error)) error { + pages := make(map[uint64]struct{}) + for address := range functions { + pages[address-address%pageSize] = struct{}{} + end := address + 15 + pages[end-end%pageSize] = struct{}{} + } + + for address := range pages { + if err := engine.MemMap(address, pageSize); err != nil { + return fmt.Errorf("map function page at %#x: %w", address, err) + } + } + + for address, name := range functions { + target, err := resolve(name) + if err != nil { + return fmt.Errorf("resolve %s: %w", name, err) + } + + trampoline := make([]byte, 16) + binary.LittleEndian.PutUint32(trampoline[0:4], 0x58000050) // ldr x16, #8 + binary.LittleEndian.PutUint32(trampoline[4:8], 0xd61f0200) // br x16 + binary.LittleEndian.PutUint64(trampoline[8:16], target) + + if err := engine.MemWrite(address, trampoline); err != nil { + return fmt.Errorf("write %s trampoline at %#x: %w", name, address, err) + } + } + + return nil +} + +func loadPrelinkedSlots(engine *unicorn.Engine, slots map[uint64]string, resolve func(string) (uint64, error)) error { + pages := make(map[uint64]struct{}) + for address := range slots { + pages[address-address%pageSize] = struct{}{} + } + + for address := range pages { + if err := engine.MemMap(address, pageSize); err != nil { + return fmt.Errorf("map slot page at %#x: %w", address, err) + } + } + + for address, name := range slots { + value, err := resolve(name) + if err != nil { + return fmt.Errorf("resolve %s: %w", name, err) + } + + var data [8]byte + + binary.LittleEndian.PutUint64(data[:], value) + + if err := engine.MemWrite(address, data[:]); err != nil { + return fmt.Errorf("write %s slot at %#x: %w", name, address, err) + } + } + + return nil +} + func (m *Machine) Initialize(hardwareID []byte) (uint64, error) { hardware, err := hardwareBlock(hardwareID) if err != nil { @@ -402,6 +482,7 @@ func (m *Machine) dispose(output uint64) error { return nil } +//nolint:wsl // Register setup and compatibility retries are clearer in execution order. func (m *Machine) invoke(function uint64, arguments ...uint64) (uint64, error) { if m.closed { return 0, errors.New("SAP guest machine is closed") @@ -412,12 +493,14 @@ func (m *Machine) invoke(function uint64, arguments ...uint64) (uint64, error) { } registers := [...]int{ - unicorn.RegRDI, - unicorn.RegRSI, - unicorn.RegRDX, - unicorn.RegRCX, - unicorn.RegR8, - unicorn.RegR9, + unicorn.RegX0, + unicorn.RegX1, + unicorn.RegX2, + unicorn.RegX3, + unicorn.RegX4, + unicorn.RegX5, + unicorn.RegX6, + unicorn.RegX7, } for index, register := range registers { var value uint64 @@ -432,43 +515,70 @@ func (m *Machine) invoke(function uint64, arguments ...uint64) (uint64, error) { extra := max(len(arguments)-len(registers), 0) - stackPointer := stackEnd - uint64(extra+1)*8 - if stackPointer%16 != 8 { - stackPointer -= 8 - } - - if err := m.writeUint64(stackPointer, returnAddress); err != nil { - return 0, err - } + stackPointer := stackEnd - uint64(extra)*8 + stackPointer -= stackPointer % 16 for index := range extra { - if err := m.writeUint64(stackPointer+8+uint64(index)*8, arguments[len(registers)+index]); err != nil { + if err := m.writeUint64(stackPointer+uint64(index)*8, arguments[len(registers)+index]); err != nil { return 0, err } } - if err := m.engine.RegWrite(unicorn.RegRSP, stackPointer); err != nil { + if err := m.engine.RegWrite(unicorn.RegSP, stackPointer); err != nil { return 0, fmt.Errorf("write SAP guest stack register: %w", err) } + if err := m.engine.RegWrite(unicorn.RegLR, returnAddress); err != nil { + return 0, fmt.Errorf("write SAP guest return register: %w", err) + } m.services.resetFault() // SAP's cryptographic routines have input- and host-dependent instruction // counts. Bound execution by wall time without rejecting legitimate work on // slower machines for crossing a fixed instruction limit. - if err := m.engine.StartBounded(function, returnAddress, sapGuestTimeout, 0); err != nil { + next := function + + for { + executionErr := m.engine.StartBounded(next, returnAddress, sapGuestTimeout, 0) + if executionErr == nil { + break + } if m.services.fault != nil { return 0, m.services.fault } - return 0, fmt.Errorf("execute SAP guest function: %w", err) + if m.arm64 != nil { + handled, compatibilityErr := m.arm64.emulateInvalidInstruction() + if compatibilityErr != nil { + return 0, compatibilityErr + } + if handled { + next, compatibilityErr = m.engine.RegRead(unicorn.RegPC) + if compatibilityErr != nil { + return 0, fmt.Errorf("resume SAP guest function: %w", compatibilityErr) + } + + continue + } + } + + instruction, readErr := m.engine.RegRead(unicorn.RegPC) + if readErr != nil { + return 0, fmt.Errorf("execute SAP guest function: %w", executionErr) + } + link, linkErr := m.engine.RegRead(unicorn.RegLR) + if linkErr != nil { + return 0, fmt.Errorf("execute SAP guest function at %#x: %w", instruction, executionErr) + } + + return 0, fmt.Errorf("execute SAP guest function at %#x from %#x: %w", instruction, link, executionErr) } if m.services.fault != nil { return 0, m.services.fault } - instruction, err := m.engine.RegRead(unicorn.RegRIP) + instruction, err := m.engine.RegRead(unicorn.RegPC) if err != nil { return 0, fmt.Errorf("read SAP guest instruction register: %w", err) } @@ -477,7 +587,7 @@ func (m *Machine) invoke(function uint64, arguments ...uint64) (uint64, error) { return 0, fmt.Errorf("SAP guest stopped unexpectedly at %#x", instruction) } - result, err := m.engine.RegRead(unicorn.RegRAX) + result, err := m.engine.RegRead(unicorn.RegX0) if err != nil { return 0, fmt.Errorf("read SAP guest result register: %w", err) } diff --git a/internal/sap/machine/machine_test.go b/internal/sap/machine/machine_test.go index 9d05be11..5e79d419 100644 --- a/internal/sap/machine/machine_test.go +++ b/internal/sap/machine/machine_test.go @@ -294,7 +294,7 @@ func newServiceTestMachine(t *testing.T) *Machine { } } - if err := engine.MemWrite(returnAddress, []byte{0xF4}); err != nil { + if err := engine.MemWrite(returnAddress, []byte{0x00, 0x00, 0x20, 0xd4}); err != nil { _ = engine.Close() t.Fatal(err) diff --git a/internal/sap/machine/macos27.go b/internal/sap/machine/macos27.go new file mode 100644 index 00000000..e53a0004 --- /dev/null +++ b/internal/sap/machine/macos27.go @@ -0,0 +1,37 @@ +package machine + +// macOS27SharedCacheFunctions maps the external function pointers used by the +// AppleMediaServices image extracted from the macOS 27.0.1 (26A434) arm64e +// shared cache. The extraction has its cache slide applied, so these values are +// stable for this exact build and are replaced with emulator services at load +// time. +var macOS27SharedCacheFunctions = map[uint64]string{ + 0x0000000180675ce8: "_malloc", + 0x00000001806766f0: "_free", + 0x00000001806cf570: "_dispatch_once", + 0x000000018071acc8: "_sysctlbyname", + 0x000000018071bec4: "_gettimeofday", + 0x0000000180721fb8: "_arc4random", + 0x00000001807238a4: "___memset_chk", + 0x00000001807903c4: "_abort", + 0x0000000180849604: "___error", + 0x0000000180889bc4: "_pthread_once", + 0x000000018088a6f8: "_pthread_rwlock_unlock", + 0x000000018088a7c0: "_pthread_rwlock_wrlock", + 0x000000018088b2d8: "_pthread_rwlock_init", + 0x0000000180894b40: "_strlen", + 0x00000001808cf540: "_CFStringCreateWithCString", + 0x00000001808d4200: "_CFStringGetLength", + 0x00000001808d44dc: "_CFStringGetCString", + 0x00000001808d7278: "_CFRelease", + 0x00000001808e89e8: "_CFDataGetLength", + 0x0000000184d6b4cc: "_IOServiceMatching", + 0x0000000184d6c168: "_IOServiceGetMatchingServices", + 0x0000000184d6c4fc: "_IOIteratorNext", + 0x0000000184d6cb50: "_IOObjectRelease", +} + +var macOS27SharedCacheSlots = map[uint64]string{ + 0x00000001e02205c0: "___stack_chk_guard", + 0x00000001e800c958: "___chkstk_darwin", +} diff --git a/internal/sap/machine/shim_memory.go b/internal/sap/machine/shim_memory.go index 47afb1a9..4e49a808 100644 --- a/internal/sap/machine/shim_memory.go +++ b/internal/sap/machine/shim_memory.go @@ -21,6 +21,10 @@ func (s *shims) registerMemoryServices() error { {[]string{"_calloc"}, s.calloc}, {[]string{"_realloc", "_reallocf"}, s.realloc}, {[]string{"_free"}, s.free}, + {[]string{"_mmap"}, s.mmap}, + {[]string{"_munmap"}, s.munmap}, + {[]string{"_vm_allocate"}, s.vmAllocate}, + {[]string{"_vm_deallocate"}, s.vmDeallocate}, {[]string{"_memcpy", "_memmove"}, s.memmove}, {[]string{"_memset"}, s.memset}, {[]string{"___bzero"}, s.bzero}, @@ -218,6 +222,102 @@ func (s *shims) allocate(size uint64) (uint64, error) { return address, nil } +func (s *shims) allocateAligned(size, alignment uint64) (uint64, error) { + if size > maxGuestTransfer { + return 0, fmt.Errorf("allocation size %d exceeds limit", size) + } + + reserved := align(max(size, 1), alignment) + + cursor := align(s.heapCursor, alignment) + if cursor > heapSize || reserved > heapSize-cursor { + return 0, errors.New("guest heap exhausted") + } + + address := heapBase + cursor + s.heapCursor = cursor + reserved + s.allocations[address] = guestAllocation{size: size, reserved: reserved} + + return address, nil +} + +func (s *shims) mmap() error { + length, err := s.argument(1) + if err != nil { + return err + } + + address, err := s.allocateAligned(length, pageSize) + if err != nil { + return s.returnMinusOne() + } + + if err := s.engine.MemWrite(address, make([]byte, length)); err != nil { + _ = s.release(address) + + return fmt.Errorf("clear mapped guest memory: %w", err) + } + + return s.setResult(address) +} + +func (s *shims) munmap() error { + address, err := s.argument(0) + if err != nil { + return err + } + + if err := s.release(address); err != nil { + return s.setResult(math.MaxUint64) + } + + return s.setResult(0) +} + +func (s *shims) vmAllocate() error { + field, err := s.argument(1) + if err != nil { + return err + } + + size, err := s.argument(2) + if err != nil { + return err + } + + address, err := s.allocateAligned(size, pageSize) + if err != nil { + return s.setResult(3) // KERN_NO_SPACE + } + + if err := s.engine.MemWrite(address, make([]byte, size)); err != nil { + _ = s.release(address) + + return fmt.Errorf("clear allocated VM memory: %w", err) + } + + if err := s.writeUint64(field, address); err != nil { + _ = s.release(address) + + return err + } + + return s.setResult(0) +} + +func (s *shims) vmDeallocate() error { + address, err := s.argument(1) + if err != nil { + return err + } + + if err := s.release(address); err != nil { + return s.setResult(1) // KERN_INVALID_ADDRESS + } + + return s.setResult(0) +} + func (s *shims) release(address uint64) error { allocation, ok := s.allocations[address] if !ok { diff --git a/internal/sap/machine/shim_platform.go b/internal/sap/machine/shim_platform.go index 45462c71..93d60771 100644 --- a/internal/sap/machine/shim_platform.go +++ b/internal/sap/machine/shim_platform.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "math" + "strings" "time" "github.com/majd/ipatool/v2/internal/sap/unicorn" @@ -33,6 +34,7 @@ func (s *shims) registerPlatformServices() error { {[]string{"_CFStringCreateWithCString"}, s.cfStringCreate}, {[]string{"_CFStringCreateWithCStringNoCopy"}, s.returnZero}, {[]string{"_CFStringGetCString"}, s.cfStringGetCString}, + {[]string{"___chkstk_darwin", "____chkstk_darwin"}, s.returnUnchanged}, {[]string{"_IOIteratorNext"}, s.ioIteratorNext}, {[]string{"_IORegistryEntryGetParentEntry"}, s.ioRegistryEntryGetParentEntry}, {[]string{"_IOServiceGetMatchingServices"}, s.ioServiceGetMatchingServices}, @@ -41,6 +43,9 @@ func (s *shims) registerPlatformServices() error { {[]string{"___error"}, s.errorPointer}, {[]string{"_abort", "___stack_chk_fail", "dyld_stub_binder"}, s.abort}, {[]string{"_arc4random"}, s.arc4random}, + {[]string{"_basename"}, s.basename}, + {[]string{"_bootstrap_look_up"}, s.bootstrapLookUp}, + {[]string{"_dispatch_once"}, s.dispatchOnce}, {[]string{"_dlopen"}, s.dlopen}, {[]string{"_dlsym"}, s.dlsym}, {[]string{"_fcntl", "_fcntl$UNIX2003", "_lstat$INODE64", "_statfs", "_statfs$INODE64"}, s.returnMinusOne}, @@ -51,6 +56,7 @@ func (s *shims) registerPlatformServices() error { {[]string{"_read", "_read$UNIX2003"}, s.read}, {[]string{"_sysctl"}, s.returnMinusOne}, {[]string{"_sysctlbyname"}, s.sysctlbyname}, + {[]string{"_sysconf"}, s.sysconf}, } for _, service := range services { if err := s.addAliases(service.names, service.handler); err != nil { @@ -80,6 +86,10 @@ func (s *shims) registerPlatformServices() error { } } + if _, err := s.addFunction("callback.complete", s.callbackComplete); err != nil { + return err + } + return nil } @@ -87,6 +97,10 @@ func (s *shims) returnZero() error { return s.setResult(0) } +func (s *shims) returnUnchanged() error { + return nil +} + func (s *shims) returnFakeHandle() error { return s.setResult(fakeHandle) } @@ -230,6 +244,40 @@ func (s *shims) arc4random() error { return s.setResult(uint64(binary.LittleEndian.Uint32(value[:]))) } +func (s *shims) basename() error { + address, err := s.argument(0) + if err != nil { + return err + } + + value, err := s.readCString(address) + if err != nil { + return err + } + + value = strings.TrimRight(value, "/") + if index := strings.LastIndexByte(value, '/'); index >= 0 { + address += uint64(index + 1) + } + + return s.setResult(address) +} + +func (s *shims) bootstrapLookUp() error { + service, err := s.argument(2) + if err != nil { + return err + } + + if service != 0 { + if err := s.writeUint32(service, 0); err != nil { + return err + } + } + + return s.setResult(5) // KERN_FAILURE +} + func (s *shims) dlopen() error { pathAddress, err := s.argument(0) if err != nil { @@ -351,26 +399,84 @@ func (s *shims) pthreadOnce() error { return err } - if value == 0 { + if value == 0 || value == 0x4f4e4345 { return s.setResult(0) } - if err := s.writeUint64(control, 0); err != nil { + if err := s.writeUint64(control, 0x4f4e4345); err != nil { return err } - stack, err := s.engine.RegRead(unicorn.RegRSP) + return s.invokeCallback(initializer) +} + +func (s *shims) dispatchOnce() error { + predicate, err := s.argument(0) if err != nil { - return fmt.Errorf("read guest stack register: %w", err) + return err } - stack -= 8 - if err := s.writeUint64(stack, initializer); err != nil { + block, err := s.argument(1) + if err != nil { return err } - if err := s.engine.RegWrite(unicorn.RegRSP, stack); err != nil { - return fmt.Errorf("write guest stack register: %w", err) + value, err := s.readUint64(predicate) + if err != nil { + return err + } + + if value == math.MaxUint64 { + return s.setResult(0) + } + + if err := s.writeUint64(predicate, math.MaxUint64); err != nil { + return err + } + + initializer, err := s.readUint64(block + 16) + if err != nil { + return err + } + + if err := s.engine.RegWrite(unicorn.RegX0, block); err != nil { + return fmt.Errorf("write dispatch block argument: %w", err) + } + + return s.invokeCallback(initializer) +} + +func (s *shims) invokeCallback(initializer uint64) error { + returnAddress, err := s.engine.RegRead(unicorn.RegLR) + if err != nil { + return fmt.Errorf("read guest return register: %w", err) + } + + s.callbackReturns = append(s.callbackReturns, returnAddress) + + completion := s.symbols["callback.complete"] + if err := s.engine.RegWrite(unicorn.RegLR, completion); err != nil { + return fmt.Errorf("write callback return register: %w", err) + } + + if err := s.engine.RegWrite(unicorn.RegPC, initializer); err != nil { + return fmt.Errorf("write callback instruction register: %w", err) + } + + return nil +} + +func (s *shims) callbackComplete() error { + if len(s.callbackReturns) == 0 { + return errors.New("guest callback completion has no return address") + } + + last := len(s.callbackReturns) - 1 + returnAddress := s.callbackReturns[last] + s.callbackReturns = s.callbackReturns[:last] + + if err := s.engine.RegWrite(unicorn.RegLR, returnAddress); err != nil { + return fmt.Errorf("restore guest return register: %w", err) } return s.setResult(0) @@ -431,3 +537,21 @@ func (s *shims) sysctlbyname() error { return s.setResult(0) } + +func (s *shims) sysconf() error { + name, err := s.argument(0) + if err != nil { + return err + } + + switch name { + case 29: // _SC_PAGESIZE + return s.setResult(pageSize) + case 57, 58: // _SC_NPROCESSORS_CONF, _SC_NPROCESSORS_ONLN + return s.setResult(1) + case 200: // _SC_PHYS_PAGES + return s.setResult(heapSize / pageSize) + default: + return s.returnMinusOne() + } +} diff --git a/internal/sap/machine/shim_platform_test.go b/internal/sap/machine/shim_platform_test.go new file mode 100644 index 00000000..d84b6cfb --- /dev/null +++ b/internal/sap/machine/shim_platform_test.go @@ -0,0 +1,64 @@ +package machine + +import ( + "math" + + "github.com/majd/ipatool/v2/internal/sap/unicorn" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("guest once services", func() { + It("runs a pthread initializer once with a valid call frame", func() { + machine := newGinkgoServiceMachine(shimOptions{}) + control := scratchBase + Expect(machine.services.writeUint64(control, 0x30b1bcba)).To(Succeed()) + + calls := 0 + initializer, err := machine.services.addFunction("test.pthread.initializer", func() error { + stack, readErr := machine.engine.RegRead(unicorn.RegSP) + Expect(readErr).NotTo(HaveOccurred()) + Expect(stack % 16).To(BeZero()) + calls++ + + return machine.services.setResult(0xdeadbeef) + }) + Expect(err).NotTo(HaveOccurred()) + + for range 2 { + result, invokeErr := machine.invoke(machine.services.symbols["_pthread_once"], control, initializer) + Expect(invokeErr).NotTo(HaveOccurred()) + Expect(result).To(BeZero()) + } + + Expect(calls).To(Equal(1)) + Expect(machine.services.readUint64(control)).To(Equal(uint64(0x4f4e4345))) + }) + + It("runs a dispatch block once", func() { + machine := newGinkgoServiceMachine(shimOptions{}) + predicate := scratchBase + block := scratchBase + 0x100 + calls := 0 + + initializer, err := machine.services.addFunction("test.dispatch.initializer", func() error { + argument, argumentErr := machine.services.argument(0) + Expect(argumentErr).NotTo(HaveOccurred()) + Expect(argument).To(Equal(block)) + calls++ + + return machine.services.setResult(0xdeadbeef) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(machine.services.writeUint64(block+16, initializer)).To(Succeed()) + + for range 2 { + result, invokeErr := machine.invoke(machine.services.symbols["_dispatch_once"], predicate, block) + Expect(invokeErr).NotTo(HaveOccurred()) + Expect(result).To(BeZero()) + } + + Expect(calls).To(Equal(1)) + Expect(machine.services.readUint64(predicate)).To(Equal(uint64(math.MaxUint64))) + }) +}) diff --git a/internal/sap/machine/shims.go b/internal/sap/machine/shims.go index d9260114..24d762d1 100644 --- a/internal/sap/machine/shims.go +++ b/internal/sap/machine/shims.go @@ -10,7 +10,7 @@ import ( ) const ( - shimBase = uint64(0x0000200000000000) + shimBase = uint64(0x00007ff880000000) shimCodeSize = uint64(0x0000000000080000) shimSize = uint64(0x0000000000100000) shimSlotSize = uint64(16) @@ -38,21 +38,22 @@ type shimOptions struct { } type shims struct { - engine *unicorn.Engine - hook *unicorn.Hook - entries map[uint64]shimEntry - symbols map[string]uint64 - codeCursor uint64 - dataCursor uint64 - fault error - coreExports map[string]uint64 - icxs []byte - icxsOffset int - errno uint64 - heapCursor uint64 - allocations map[uint64]guestAllocation - freeBlocks []freeBlock - iterator uint32 + engine *unicorn.Engine + hook *unicorn.Hook + entries map[uint64]shimEntry + symbols map[string]uint64 + codeCursor uint64 + dataCursor uint64 + fault error + coreExports map[string]uint64 + icxs []byte + icxsOffset int + errno uint64 + heapCursor uint64 + allocations map[uint64]guestAllocation + freeBlocks []freeBlock + iterator uint32 + callbackReturns []uint64 } func newShims(engine *unicorn.Engine, coreExports map[string]uint64, icxs []byte) (*shims, error) { @@ -143,7 +144,7 @@ func (s *shims) addFunction(name string, handler shimHandler) (uint64, error) { address := s.codeCursor s.codeCursor += shimSlotSize - if err := s.engine.MemWrite(address, []byte{0xC3}); err != nil { + if err := s.engine.MemWrite(address, []byte{0xc0, 0x03, 0x5f, 0xd6}); err != nil { return 0, fmt.Errorf("write guest service stub: %w", err) } @@ -204,12 +205,14 @@ func (s *shims) resetFault() { func (s *shims) argument(index int) (uint64, error) { registers := [...]int{ - unicorn.RegRDI, - unicorn.RegRSI, - unicorn.RegRDX, - unicorn.RegRCX, - unicorn.RegR8, - unicorn.RegR9, + unicorn.RegX0, + unicorn.RegX1, + unicorn.RegX2, + unicorn.RegX3, + unicorn.RegX4, + unicorn.RegX5, + unicorn.RegX6, + unicorn.RegX7, } if index >= 0 && index < len(registers) { value, err := s.engine.RegRead(registers[index]) @@ -224,12 +227,12 @@ func (s *shims) argument(index int) (uint64, error) { return 0, errors.New("negative guest argument index") } - stack, err := s.engine.RegRead(unicorn.RegRSP) + stack, err := s.engine.RegRead(unicorn.RegSP) if err != nil { return 0, fmt.Errorf("read guest stack register: %w", err) } - data, err := s.engine.MemRead(stack+8+uint64(index-len(registers))*8, 8) + data, err := s.engine.MemRead(stack+uint64(index-len(registers))*8, 8) if err != nil { return 0, fmt.Errorf("read guest stack argument: %w", err) } @@ -238,7 +241,7 @@ func (s *shims) argument(index int) (uint64, error) { } func (s *shims) setResult(value uint64) error { - if err := s.engine.RegWrite(unicorn.RegRAX, value); err != nil { + if err := s.engine.RegWrite(unicorn.RegX0, value); err != nil { return fmt.Errorf("write guest result register: %w", err) } diff --git a/internal/sap/unicorn/engine.go b/internal/sap/unicorn/engine.go index ad35dac0..01c39b5c 100644 --- a/internal/sap/unicorn/engine.go +++ b/internal/sap/unicorn/engine.go @@ -12,22 +12,33 @@ import ( "github.com/ebitengine/purego" ) -const queryTimeout = 4 +const ( + queryTimeout = 4 + controlCPUModel = uint32(7 | 1<<26 | 1<<30) + arm64CPUModelMax = uint32(3) +) const ( - archX86 = 4 - mode64 = 8 - protAll = 7 - - RegRAX = 35 - RegRCX = 38 - RegRDI = 39 - RegRDX = 40 - RegRIP = 41 - RegRSI = 43 - RegRSP = 44 - RegR8 = 106 - RegR9 = 107 + archARM64 = 2 + modeARM = 0 + protAll = 7 + + RegX29 = 1 + RegLR = 2 + RegSP = 4 + RegX0 = 199 + RegX1 = 200 + RegX2 = 201 + RegX3 = 202 + RegX4 = 203 + RegX5 = 204 + RegX6 = 205 + RegX7 = 206 + RegX14 = 213 + RegX15 = 214 + RegX16 = 215 + RegV0 = 228 + RegPC = 260 ) var ( @@ -108,10 +119,19 @@ func newEngine(ctx context.Context, loadLibrary func(context.Context) (library, return nil, fmt.Errorf("unsupported Unicorn API version %d.%d", major, minor) } - if err := engine.err(engine.api.open(archX86, mode64, &engine.handle)); err != nil { + if err := engine.err(engine.api.open(archARM64, modeARM, &engine.handle)); err != nil { + _ = library.close() + + return nil, fmt.Errorf("create arm64 emulator: %w", err) + } + + // Select Unicorn's most capable ARM64 model. The compatibility layer handles + // modern instructions that this Unicorn build does not implement. + if err := engine.err(engine.api.ctl(engine.handle, controlCPUModel, arm64CPUModelMax)); err != nil { + _ = engine.api.close(engine.handle) _ = library.close() - return nil, fmt.Errorf("create x86-64 emulator: %w", err) + return nil, fmt.Errorf("configure arm64 CPU model: %w", err) } if err := configureEngine(engine); err != nil { @@ -238,6 +258,29 @@ func (e *Engine) RegWrite(register int, value uint64) error { return e.err(e.api.regWrite(handle, int32(register), unsafe.Pointer(&value))) } +func (e *Engine) RegRead128(register int) ([16]byte, error) { + handle, done, err := e.beginOperation() + if err != nil { + return [16]byte{}, err + } + defer done() + + var value [16]byte + err = e.err(e.api.regRead(handle, int32(register), unsafe.Pointer(&value[0]))) + + return value, err +} + +func (e *Engine) RegWrite128(register int, value [16]byte) error { + handle, done, err := e.beginOperation() + if err != nil { + return err + } + defer done() + + return e.err(e.api.regWrite(handle, int32(register), unsafe.Pointer(&value[0]))) +} + func (e *Engine) Start(begin, end uint64) error { handle, done, err := e.beginOperation() if err != nil { diff --git a/internal/sap/unicorn/engine_test.go b/internal/sap/unicorn/engine_test.go index 0da94e90..2c8a27ca 100644 --- a/internal/sap/unicorn/engine_test.go +++ b/internal/sap/unicorn/engine_test.go @@ -105,11 +105,11 @@ func TestEngineOperationsFailAfterClose(t *testing.T) { {"MemReadInto", func() error { return engine.MemReadInto(nil, 0) }}, {"MemWrite", func() error { return engine.MemWrite(0, nil) }}, {"RegRead", func() error { - _, err := engine.RegRead(RegRAX) + _, err := engine.RegRead(RegX0) return err }}, - {"RegWrite", func() error { return engine.RegWrite(RegRAX, 0) }}, + {"RegWrite", func() error { return engine.RegWrite(RegX0, 0) }}, {"Start", func() error { return engine.Start(0, 1) }}, {"StartBounded", func() error { return engine.StartBounded(0, 1, time.Second, 1) }}, {"Stop", engine.Stop}, @@ -214,7 +214,7 @@ func waitForResult(t *testing.T, result <-chan error) { } } -func TestEngineExecutesX8664(t *testing.T) { +func TestEngineExecutesARM64(t *testing.T) { engine := newTestEngine(t) defer engine.Close() @@ -223,8 +223,8 @@ func TestEngineExecutesX8664(t *testing.T) { t.Fatal(err) } - // mov rax, 0x1234; hlt - code := []byte{0x48, 0xc7, 0xc0, 0x34, 0x12, 0x00, 0x00, 0xf4} + // mov x0, #0x1234 + code := []byte{0x80, 0x46, 0x82, 0xd2} if err := engine.MemWrite(address, code); err != nil { t.Fatal(err) } @@ -233,13 +233,13 @@ func TestEngineExecutesX8664(t *testing.T) { t.Fatal(err) } - value, err := engine.RegRead(RegRAX) + value, err := engine.RegRead(RegX0) if err != nil { t.Fatal(err) } if value != 0x1234 { - t.Fatalf("RAX = %#x, want %#x", value, uint64(0x1234)) + t.Fatalf("X0 = %#x, want %#x", value, uint64(0x1234)) } } @@ -252,22 +252,22 @@ func TestStartBoundedReportsTimeout(t *testing.T) { t.Fatal(err) } - // jmp $-2 - if err := engine.MemWrite(address, []byte{0xeb, 0xfe}); err != nil { + // b . + if err := engine.MemWrite(address, []byte{0x00, 0x00, 0x00, 0x14}); err != nil { t.Fatal(err) } - err := engine.StartBounded(address, address+2, 10*time.Millisecond, 0) + err := engine.StartBounded(address, address+4, 10*time.Millisecond, 0) if !errors.Is(err, errTimeout) { t.Fatalf("StartBounded error = %v, want %v", err, errTimeout) } const completionAddress = address + 0x10 - if err := engine.MemWrite(completionAddress, []byte{0xf4}); err != nil { + if err := engine.MemWrite(completionAddress, []byte{0x1f, 0x20, 0x03, 0xd5}); err != nil { t.Fatal(err) } - if err := engine.StartBounded(completionAddress, completionAddress+1, time.Second, 0); err != nil { + if err := engine.StartBounded(completionAddress, completionAddress+4, time.Second, 0); err != nil { t.Fatalf("StartBounded after timeout: %v", err) } } diff --git a/internal/sap/unicorn/hook_test.go b/internal/sap/unicorn/hook_test.go index f290eb2e..073d00ef 100644 --- a/internal/sap/unicorn/hook_test.go +++ b/internal/sap/unicorn/hook_test.go @@ -11,8 +11,8 @@ func TestCodeHookStopsEmulation(t *testing.T) { t.Fatal(err) } - // mov rax, 1; inc rax; ud2 - code := []byte{0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00, 0x48, 0xff, 0xc0, 0x0f, 0x0b} + // mov x0, #1; add x0, x0, #1; brk #0 + code := []byte{0x20, 0x00, 0x80, 0xd2, 0x00, 0x04, 0x00, 0x91, 0x00, 0x00, 0x20, 0xd4} if err := engine.MemWrite(address, code); err != nil { t.Fatal(err) } @@ -24,7 +24,7 @@ func TestCodeHookStopsEmulation(t *testing.T) { stopErr error ) - hook, err := engine.AddCodeHook(address+7, address+7, func(current uint64, size uint32) { + hook, err := engine.AddCodeHook(address+4, address+4, func(current uint64, size uint32) { calls++ hookAddress = current hookSize = size @@ -48,8 +48,8 @@ func TestCodeHookStopsEmulation(t *testing.T) { t.Fatalf("callback calls = %d, want 1", calls) } - if hookAddress != address+7 || hookSize != 3 { - t.Fatalf("callback instruction = (%#x, %d), want (%#x, 3)", hookAddress, hookSize, address+7) + if hookAddress != address+4 || hookSize != 4 { + t.Fatalf("callback instruction = (%#x, %d), want (%#x, 4)", hookAddress, hookSize, address+4) } } @@ -62,7 +62,7 @@ func TestClosedCodeHookDoesNotRun(t *testing.T) { t.Fatal(err) } - if err := engine.MemWrite(address, []byte{0x90, 0xf4}); err != nil { + if err := engine.MemWrite(address, []byte{0x1f, 0x20, 0x03, 0xd5}); err != nil { t.Fatal(err) } @@ -83,7 +83,7 @@ func TestClosedCodeHookDoesNotRun(t *testing.T) { t.Fatal(err) } - if err := engine.Start(address, address+2); err != nil { + if err := engine.Start(address, address+4); err != nil { t.Fatal(err) } From 70feb8d23b7338bc14f7cad1e9e2d090bbef0428 Mon Sep 17 00:00:00 2001 From: Majd Alfhaily Date: Sat, 10 Oct 2026 18:31:23 +0200 Subject: [PATCH 4/4] feat: migrate sap services to macos 27 --- internal/sap/machine/kbsync.go | 7 +--- internal/sap/machine/kbsync_test.go | 2 +- internal/sap/machine/storeagent.go | 45 +++++++++++-------------- internal/sap/machine/storeagent_test.go | 28 ++++++--------- 4 files changed, 33 insertions(+), 49 deletions(-) diff --git a/internal/sap/machine/kbsync.go b/internal/sap/machine/kbsync.go index 017562c6..e12d3569 100644 --- a/internal/sap/machine/kbsync.go +++ b/internal/sap/machine/kbsync.go @@ -23,12 +23,7 @@ func GenerateKBSync(ctx context.Context, bundle assets.Bundle, hardwareID []byte return nil, errors.New("kbsync requires a nonzero account DSID") } - image, err := assets.LoadStoreAgent(ctx) - if err != nil { - return nil, fmt.Errorf("load Apple StoreAgent asset: %w", err) - } - - agent, globalContext, err := openStoreAgentGlobal(ctx, bundle, image, hardwareID) + agent, globalContext, err := openStoreAgentGlobal(ctx, bundle, hardwareID) if err != nil { return nil, err } diff --git a/internal/sap/machine/kbsync_test.go b/internal/sap/machine/kbsync_test.go index cbe44238..03a79037 100644 --- a/internal/sap/machine/kbsync_test.go +++ b/internal/sap/machine/kbsync_test.go @@ -11,7 +11,7 @@ import ( var _ = Describe("StoreAgent kbsync", func() { It("pins the kbsync entry in the verified StoreAgent image", func() { - Expect(storeAgentKBSyncEntry).To(Equal(storeAgentBase + 0x0c93c0)) + Expect(storeAgentKBSyncEntry).To(Equal(storeAgentBase + 0x19ae34)) }) It("rejects a canceled context before loading assets", func() { diff --git a/internal/sap/machine/storeagent.go b/internal/sap/machine/storeagent.go index b1301863..e6c2c317 100644 --- a/internal/sap/machine/storeagent.go +++ b/internal/sap/machine/storeagent.go @@ -12,11 +12,11 @@ import ( const ( storeAgentBase = uint64(0x00001000c0000000) - storeAgentGlobalInit = storeAgentBase + 0x0c5fc0 - storeAgentKBSyncEntry = storeAgentBase + 0x0c93c0 - storeAgentSessionInit = storeAgentBase + 0x0debd0 - storeAgentDecryptEntry = storeAgentBase + 0x0ee700 - storeAgentSessionClose = storeAgentBase + 0x1212d0 + storeAgentGlobalInit = storeAgentBase + 0x1312fc + storeAgentKBSyncEntry = storeAgentBase + 0x19ae34 + storeAgentSessionInit = storeAgentBase + 0x13ac60 + storeAgentDecryptEntry = storeAgentBase + 0x175390 + storeAgentGlobalClose = storeAgentBase + 0x130eb4 storeAgentChunkSize = 0x8000 storeAgentSCInfoPath = "/Users/Shared/SC Info" ) @@ -32,6 +32,7 @@ var storeAgentZeroReturnAliases = []string{ type StoreAgent struct { mu sync.Mutex guest *Machine + global uint32 session uint64 decryptEntry uint64 closeEntry uint64 @@ -47,20 +48,15 @@ func OpenStoreAgent(ctx context.Context, bundle assets.Bundle, hardwareID, dpInf return nil, fmt.Errorf("open StoreAgent runtime: %w", err) } - image, err := assets.LoadStoreAgent(ctx) - if err != nil { - return nil, fmt.Errorf("load Apple StoreAgent asset: %w", err) - } - - return openStoreAgent(ctx, bundle, image, hardwareID, dpInfo) + return openStoreAgent(ctx, bundle, hardwareID, dpInfo) } -func openStoreAgent(ctx context.Context, bundle assets.Bundle, image, hardwareID, dpInfo []byte) (*StoreAgent, error) { +func openStoreAgent(ctx context.Context, bundle assets.Bundle, hardwareID, dpInfo []byte) (*StoreAgent, error) { if len(dpInfo) == 0 { return nil, errors.New("StoreAgent dpInfo is empty") } - agent, globalContext, err := openStoreAgentGlobal(ctx, bundle, image, hardwareID) + agent, globalContext, err := openStoreAgentGlobal(ctx, bundle, hardwareID) if err != nil { return nil, err } @@ -75,7 +71,7 @@ func openStoreAgent(ctx context.Context, bundle assets.Bundle, image, hardwareID return agent, nil } -func openStoreAgentGlobal(ctx context.Context, bundle assets.Bundle, image, hardwareID []byte) (*StoreAgent, uint32, error) { +func openStoreAgentGlobal(ctx context.Context, bundle assets.Bundle, hardwareID []byte) (*StoreAgent, uint32, error) { if ctx == nil { return nil, 0, errors.New("StoreAgent context is nil") } @@ -84,17 +80,13 @@ func openStoreAgentGlobal(ctx context.Context, bundle assets.Bundle, image, hard return nil, 0, fmt.Errorf("open StoreAgent runtime: %w", err) } - if err := assets.VerifyStoreAgent(image); err != nil { - return nil, 0, fmt.Errorf("verify Apple StoreAgent profile: %w", err) - } - hardware, err := hardwareBlock(hardwareID) if err != nil { return nil, 0, err } guest, exports, err := openRuntime(ctx, bundle, runtimeOptions{ - extraImages: []imageSpec{{name: "storeagent", data: image, base: storeAgentBase}}, + extraImages: []imageSpec{{name: "commerce", data: bundle.Commerce, base: storeAgentBase}}, shims: shimOptions{ zeroReturnAliases: storeAgentZeroReturnAliases, }, @@ -108,7 +100,7 @@ func openStoreAgentGlobal(ctx context.Context, bundle assets.Bundle, image, hard agent := &StoreAgent{ guest: guest, decryptEntry: storeAgentDecryptEntry, - closeEntry: storeAgentSessionClose, + closeEntry: storeAgentGlobalClose, } globalContext, err := agent.initializeGlobal(hardware) @@ -118,6 +110,8 @@ func openStoreAgentGlobal(ctx context.Context, bundle assets.Bundle, image, hard return nil, 0, err } + agent.global = globalContext + return agent, globalContext, nil } @@ -319,15 +313,16 @@ func (s *StoreAgent) Close() error { var errs []error - if s.guest != nil && s.session != 0 { - session := s.session + if s.guest != nil && s.global != 0 { + global := s.global + s.global = 0 s.session = 0 - status, err := s.guest.invoke(s.closeEntry, session) + status, err := s.guest.invoke(s.closeEntry, uint64(global)) if err != nil { - errs = append(errs, fmt.Errorf("close StoreAgent session: %w", err)) + errs = append(errs, fmt.Errorf("close StoreAgent global context: %w", err)) } else if int32(status) != 0 { - errs = append(errs, fmt.Errorf("StoreAgent session close returned %d", int32(status))) + errs = append(errs, fmt.Errorf("StoreAgent global context close returned %d", int32(status))) } } diff --git a/internal/sap/machine/storeagent_test.go b/internal/sap/machine/storeagent_test.go index 9a782b40..0df72dbe 100644 --- a/internal/sap/machine/storeagent_test.go +++ b/internal/sap/machine/storeagent_test.go @@ -6,7 +6,6 @@ import ( "errors" "io" - "github.com/majd/ipatool/v2/internal/sap/assets" "github.com/majd/ipatool/v2/internal/sap/unicorn" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -16,16 +15,10 @@ var _ = Describe("StoreAgent", func() { Describe("profile", func() { It("pins the verified image base and entry offsets", func() { Expect(storeAgentBase).To(Equal(uint64(0x00001000c0000000))) - Expect(storeAgentGlobalInit).To(Equal(storeAgentBase + 0x0c5fc0)) - Expect(storeAgentSessionInit).To(Equal(storeAgentBase + 0x0debd0)) - Expect(storeAgentDecryptEntry).To(Equal(storeAgentBase + 0x0ee700)) - Expect(storeAgentSessionClose).To(Equal(storeAgentBase + 0x1212d0)) - }) - - It("re-verifies the pinned image before opening the runtime", func() { - _, err := openStoreAgent(context.Background(), assets.Bundle{}, []byte("wrong image"), []byte{1}, []byte{1}) - Expect(err).To(MatchError(ContainSubstring("verify Apple StoreAgent profile"))) - Expect(err).To(MatchError(ContainSubstring("has size"))) + Expect(storeAgentGlobalInit).To(Equal(storeAgentBase + 0x1312fc)) + Expect(storeAgentSessionInit).To(Equal(storeAgentBase + 0x13ac60)) + Expect(storeAgentDecryptEntry).To(Equal(storeAgentBase + 0x175390)) + Expect(storeAgentGlobalClose).To(Equal(storeAgentBase + 0x130eb4)) }) It("adds only the StoreAgent synchronization aliases", func() { @@ -135,24 +128,25 @@ var _ = Describe("StoreAgent", func() { }) }) - It("closes the session and machine idempotently", func() { + It("closes the global context and machine idempotently", func() { machine := newGinkgoServiceMachine(shimOptions{}) - var closedSession uint64 + var closedContext uint64 closeEntry, err := machine.services.addFunction("test.storeagent.close", func() error { value, argumentErr := machine.services.argument(0) if argumentErr != nil { return argumentErr } - closedSession = value + closedContext = value return machine.services.setResult(0) }) Expect(err).NotTo(HaveOccurred()) - agent := &StoreAgent{guest: machine, session: 0x1234, closeEntry: closeEntry} + agent := &StoreAgent{guest: machine, global: 0x1234, session: 0x5678, closeEntry: closeEntry} Expect(agent.Close()).To(Succeed()) - Expect(closedSession).To(Equal(uint64(0x1234))) + Expect(closedContext).To(Equal(uint64(0x1234))) + Expect(agent.global).To(BeZero()) Expect(agent.session).To(BeZero()) Expect(agent.guest).To(BeNil()) Expect(agent.Close()).To(Succeed()) @@ -189,7 +183,7 @@ func newGinkgoServiceMachine(options shimOptions) *Machine { Expect(engine.MemMap(region.address, region.size)).To(Succeed()) } - Expect(engine.MemWrite(returnAddress, []byte{0xF4})).To(Succeed()) + Expect(engine.MemWrite(returnAddress, []byte{0x00, 0x00, 0x20, 0xd4})).To(Succeed()) services, err := newShimsWithOptions(engine, map[string]uint64{}, nil, options) Expect(err).NotTo(HaveOccurred())