From 36f2a8e7daa0dfdffb7468bc0b8b4eda9206beed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20L=C3=BCdeke?= Date: Thu, 2 Jul 2026 13:59:43 +0200 Subject: [PATCH 1/2] fix: exit with code 1 when DDI mount fails Previously returned cleanly (exit 0) on mount error, silently bypassing set -e in launch.sh and letting the pod continue with no testmanagerd. Co-Authored-By: Claude Sonnet 4.6 --- cmd_device_debug.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd_device_debug.go b/cmd_device_debug.go index afaad1dd..8b6bc048 100644 --- a/cmd_device_debug.go +++ b/cmd_device_debug.go @@ -142,7 +142,7 @@ func runImageCommand(ctx commandContext) { err := imagemounter.MountImage(ctx.Device, imagePath) if err != nil { slog.Error("error mounting image", "image", imagePath, "udid", ctx.Device.Properties.SerialNumber, "err", err) - return + os.Exit(1) } slog.Info("success mounting image", "image", imagePath, "udid", ctx.Device.Properties.SerialNumber) } From d6fdcbec0e21d0c92cbb5cb451791068d072674c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20L=C3=BCdeke?= Date: Thu, 2 Jul 2026 14:52:34 +0200 Subject: [PATCH 2/2] fix(imagemounter): derive EPRO/ESEC from RestoreRequestRules for TSS Personalized DDI mounts failed on fresh devices with Apple TSS status 94 ("This device isn't eligible for the requested build."). Modern developer-disk-image build manifests no longer carry literal EPRO/ESEC values on their components (LoadableTrustCache, PersonalizedDMG). Those flags are expressed via the LoadableTrustCache component's RestoreRequestRules, evaluated against the device's personalization parameters (production mode, security mode, img4 support). We read the absent literals as Go's zero value and sent EPRO=false, ESEC=false, which TSS rejects for a production device. The failure was masked on devices that already had an on-device personalization manifest cached (QueryPersonalizationManifest), since those skip TSS entirely. It only surfaced on freshly-provisioned devices that must personalize via TSS. Fix: - Parse RestoreRequestRules and make EPRO/ESEC pointers so absent is distinguishable from false. - Apply the rules to each trusted manifest entry using the device's parameters, mirroring Apple's libauthinstall / pymobiledevice3. - Surface the TSS response message in the error to make future failures diagnosable. Adds device-free unit tests for the rule evaluation. Co-Authored-By: Claude Opus 4.8 (1M context) --- ios/imagemounter/personalized_image.go | 75 +++++++++++++++- .../restore_request_rules_test.go | 87 +++++++++++++++++++ ios/imagemounter/tss.go | 27 +++++- 3 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 ios/imagemounter/restore_request_rules_test.go diff --git a/ios/imagemounter/personalized_image.go b/ios/imagemounter/personalized_image.go index d35eadd9..e0b19ba8 100644 --- a/ios/imagemounter/personalized_image.go +++ b/ios/imagemounter/personalized_image.go @@ -40,15 +40,82 @@ func (m buildManifest) findIdentity(identifiers personalizationIdentifiers) (bui type manifestEntry struct { Digest []byte - Trusted bool `plist:"Trusted"` - EPRO bool `plist:"EPRO"` - ESEC bool `plist:"ESEC"` + Trusted bool `plist:"Trusted"` + EPRO *bool `plist:"EPRO"` + ESEC *bool `plist:"ESEC"` Name string Info struct { - Path string + Path string + RestoreRequestRules []restoreRequestRule `plist:"RestoreRequestRules"` } } +// restoreRequestRule gates the EPRO/ESEC flags of a manifest component on the +// device's personalization parameters (production mode, security mode, …). +// Modern developer-disk-image build manifests omit literal EPRO/ESEC values and +// express them through these rules instead. +type restoreRequestRule struct { + Actions map[string]interface{} `plist:"Actions"` + Conditions map[string]interface{} `plist:"Conditions"` +} + +// restoreRequestRules returns the rules that determine the EPRO/ESEC flags for +// the TSS personalization request. Apple's libauthinstall (and pymobiledevice3) +// take these from the LoadableTrustCache component and apply them to every +// trusted component of a developer disk image. +func (b buildIdentity) restoreRequestRules() []restoreRequestRule { + if entry, ok := b.Manifest["LoadableTrustCache"]; ok { + return entry.Info.RestoreRequestRules + } + return nil +} + +// applyRestoreRequestRules mutates a TSS manifest entry, applying the actions of +// every rule whose conditions are satisfied by params. This is how EPRO/ESEC end +// up on the request: e.g. a device in production + secure mode gets EPRO=true and +// ESEC=true even though the build manifest carries no literal values. Mirrors the +// rule evaluation in Apple's libauthinstall / pymobiledevice3. +func applyRestoreRequestRules(entry, params map[string]interface{}, rules []restoreRequestRule) { + for _, rule := range rules { + if !restoreRuleConditionsMet(rule.Conditions, params) { + continue + } + for key, value := range rule.Actions { + // 255 is a "leave unchanged" sentinel used by some components. + if v, ok := value.(uint64); ok && v == 255 { + continue + } + entry[key] = value + } + } +} + +func restoreRuleConditionsMet(conditions, params map[string]interface{}) bool { + for key, want := range conditions { + var got interface{} + switch key { + case "ApRawProductionMode", "ApCurrentProductionMode": + got = params["ApProductionMode"] + case "ApRawSecurityMode": + got = params["ApSecurityMode"] + case "ApRequiresImage4": + got = params["ApSupportsImg4"] + case "ApDemotionPolicyOverride": + got = params["DemotionPolicy"] + case "ApInRomDFU": + got = params["ApInRomDFU"] + default: + // Unknown condition: treat the rule as unmatched, matching the + // reference implementations rather than guessing. + return false + } + if got == nil || want != got { + return false + } + } + return true +} + type buildIdentity struct { BoardID string `plist:"ApBoardID"` ChipID string `plist:"ApChipID"` diff --git a/ios/imagemounter/restore_request_rules_test.go b/ios/imagemounter/restore_request_rules_test.go new file mode 100644 index 00000000..258dfeec --- /dev/null +++ b/ios/imagemounter/restore_request_rules_test.go @@ -0,0 +1,87 @@ +package imagemounter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func boolPtr(b bool) *bool { return &b } + +// ddiTrustCacheRules mirrors the RestoreRequestRules that modern developer-disk-image +// build manifests attach to the LoadableTrustCache component. EPRO/ESEC are not literal +// values in the manifest; they are derived from these rules and the device's parameters. +func ddiTrustCacheRules() []restoreRequestRule { + return []restoreRequestRule{ + {Actions: map[string]interface{}{"EPRO": false}, Conditions: map[string]interface{}{"ApCurrentProductionMode": false, "ApRequiresImage4": true}}, + {Actions: map[string]interface{}{"EPRO": true}, Conditions: map[string]interface{}{"ApCurrentProductionMode": true, "ApRequiresImage4": true}}, + {Actions: map[string]interface{}{"ESEC": false}, Conditions: map[string]interface{}{"ApRawSecurityMode": false, "ApRequiresImage4": true}}, + {Actions: map[string]interface{}{"ESEC": true}, Conditions: map[string]interface{}{"ApRawSecurityMode": true, "ApRequiresImage4": true}}, + } +} + +// A production, secure device must end up with EPRO=true and ESEC=true, otherwise +// Apple TSS rejects the request with status 94 ("This device isn't eligible for the +// requested build."). +func TestApplyRestoreRequestRulesProductionSecure(t *testing.T) { + params := map[string]interface{}{ + "ApProductionMode": true, + "ApSecurityMode": true, + "ApSupportsImg4": true, + } + entry := map[string]interface{}{"Digest": []byte{1, 2, 3}, "Trusted": true} + + applyRestoreRequestRules(entry, params, ddiTrustCacheRules()) + + assert.Equal(t, true, entry["EPRO"]) + assert.Equal(t, true, entry["ESEC"]) +} + +// A non-production, non-secure device (e.g. a demoted / development unit) must get +// EPRO=false and ESEC=false from the same rules. +func TestApplyRestoreRequestRulesDevelopment(t *testing.T) { + params := map[string]interface{}{ + "ApProductionMode": false, + "ApSecurityMode": false, + "ApSupportsImg4": true, + } + entry := map[string]interface{}{} + + applyRestoreRequestRules(entry, params, ddiTrustCacheRules()) + + assert.Equal(t, false, entry["EPRO"]) + assert.Equal(t, false, entry["ESEC"]) +} + +// A rule referencing a condition we do not model (or whose parameter is absent) must +// not match, leaving the entry untouched rather than applying its actions. +func TestApplyRestoreRequestRulesUnmatchedConditions(t *testing.T) { + params := map[string]interface{}{"ApProductionMode": true, "ApSupportsImg4": true} + entry := map[string]interface{}{} + + rules := []restoreRequestRule{ + // ApInRomDFU is not set in params -> rule must not apply. + {Actions: map[string]interface{}{"EPRO": false}, Conditions: map[string]interface{}{"ApInRomDFU": true}}, + // Unknown condition -> rule must not apply. + {Actions: map[string]interface{}{"ESEC": true}, Conditions: map[string]interface{}{"SomeUnknownCondition": true}}, + } + + applyRestoreRequestRules(entry, params, rules) + + assert.NotContains(t, entry, "EPRO") + assert.NotContains(t, entry, "ESEC") +} + +// The 255 sentinel means "leave unchanged" and must not overwrite an existing value. +func TestApplyRestoreRequestRulesSentinel(t *testing.T) { + params := map[string]interface{}{"ApProductionMode": true, "ApSupportsImg4": true} + entry := map[string]interface{}{"EPRO": true} + + rules := []restoreRequestRule{ + {Actions: map[string]interface{}{"EPRO": uint64(255)}, Conditions: map[string]interface{}{"ApCurrentProductionMode": true, "ApRequiresImage4": true}}, + } + + applyRestoreRequestRules(entry, params, rules) + + assert.Equal(t, true, entry["EPRO"]) +} diff --git a/ios/imagemounter/tss.go b/ios/imagemounter/tss.go index 488c5376..bfb17d58 100644 --- a/ios/imagemounter/tss.go +++ b/ios/imagemounter/tss.go @@ -48,6 +48,16 @@ func (t tssClient) getSignature(identity buildIdentity, identifiers personalizat "UID_MODE": false, } + // Personalization parameters describing this device to the manifest's + // RestoreRequestRules. Developer disk images are always personalized for a + // production, secure, img4-based device. + tssParameters := map[string]interface{}{ + "ApProductionMode": true, + "ApSecurityMode": true, + "ApSupportsImg4": true, + } + rules := identity.restoreRequestRules() + for key, entry := range identity.Manifest { if !entry.Trusted { continue @@ -55,9 +65,20 @@ func (t tssClient) getSignature(identity buildIdentity, identifiers personalizat entryParams := map[string]interface{}{ "Digest": entry.Digest, "Trusted": true, - "EPRO": entry.EPRO, - "ESEC": entry.ESEC, } + // Keep any literal EPRO/ESEC the manifest provides, then let its + // RestoreRequestRules override them for this device. Modern DDI manifests + // omit the literals entirely; without applying the rules the request goes + // out with EPRO/ESEC=false and Apple TSS rejects it with status 94 + // ("This device isn't eligible for the requested build."). + if entry.EPRO != nil { + entryParams["EPRO"] = *entry.EPRO + } + if entry.ESEC != nil { + entryParams["ESEC"] = *entry.ESEC + } + applyRestoreRequestRules(entryParams, tssParameters, rules) + if key == "PersonalizedDMG" || key == "PersonalizedDmg" { if entry.Name != "" { entryParams["Name"] = entry.Name @@ -103,7 +124,7 @@ func (t tssClient) getSignature(identity buildIdentity, identifiers personalizat return nil, fmt.Errorf("getSignature: failed to parse response: %w", err) } if resp.status != 0 { - return nil, fmt.Errorf("unexpected status in response %d", resp.status) + return nil, fmt.Errorf("unexpected status in response %d: %q", resp.status, resp.message) } var ticket map[string]interface{} _, err = plist.Unmarshal([]byte(resp.requestString), &ticket)