//go:build sandbox package diff import ( "errors" "fmt" "strings" "github.com/apex/log" "github.com/blacktop/go-macho" "github.com/blacktop/go-macho/pkg/fixupchains" "github.com/blacktop/go-macho/types" "github.com/blacktop/ipsw/pkg/sandbox" "github.com/blacktop/ipsw/pkg/sandbox/normalize" ) const ( maxSandboxDiffNormalizedNodes = 1000 maxSandboxDiffOutputBytes = 32 << 20 ) func (d *Diff) parseSandboxProfiles() (string, error) { // The sandbox diff reads only the kernelcache. extractKernelcaches sets // Kernel.Path as a side effect of the kexts task's Parse, but a warm cache // hit on kexts skips that Parse, leaving the path empty on a partial hit // (kexts hydrated, sandbox missed). Ensure the path here so a fresh sandbox // parse is self-sufficient regardless of the kexts cache state. if err := d.ensureKernelcachePaths(); err != nil { return "", fmt.Errorf("failed to resolve kernelcache paths for sandbox diff: %w", err) } oldDocs, err := collectSandboxProfileDocuments(&d.Old) if err != nil { return "", fmt.Errorf("old sandbox profiles: %w", err) } newDocs, err := collectSandboxProfileDocuments(&d.New) if err != nil { return "", fmt.Errorf("new sandbox profiles: %w", err) } if len(oldDocs) == 0 && len(newDocs) == 0 { return "", nil } return renderSandboxProfileDiffMarkdown(oldDocs, newDocs) } func collectSandboxProfileDocuments(ctx *Context) (sandboxProfileDocuments, error) { if ctx.Kernel.Path == "" { return nil, fmt.Errorf("kernelcache path is empty") } kernel, err := macho.Open(ctx.Kernel.Path) if err != nil { return nil, fmt.Errorf("failed to open kernelcache: %w", err) } defer kernel.Close() var fixups map[uint64]uint64 if kernel.FileTOC.FileHeader.Type == types.MH_FILESET { fixups, err = buildSandboxDiffFixupMap(kernel) if err != nil { return nil, fmt.Errorf("failed to build fileset fixup map: %w", err) } } out := make(sandboxProfileDocuments) for _, source := range sandboxDiffSourceOrder { profiles, err := renderSandboxSourceProfiles(kernel, fixups, source) if err != nil { if isSandboxSourceUnavailable(err) { log.WithError(err).Debugf("skipping unavailable %s sandbox source", source) continue } return nil, fmt.Errorf("%s: %w", source, err) } if len(profiles) > 0 { out[source] = profiles } } return out, nil } func renderSandboxSourceProfiles(kernel *macho.File, fixups map[uint64]uint64, source string) (map[string]string, error) { conf := &sandbox.Config{Kernel: kernel, Quiet: true} if fixups != nil { conf.Fixups = fixups } sbObj, err := sandbox.NewSandbox(conf) if err != nil { return nil, fmt.Errorf("failed to create sandbox parser: %w", err) } switch source { case sandboxDiffSourceCollection: if _, err := sbObj.GetCollectionData(); err != nil { return nil, fmt.Errorf("failed to load collection data: %w", err) } if err := sbObj.ParseSandboxCollection(); err != nil { return nil, fmt.Errorf("failed to parse collection data: %w", err) } case sandboxDiffSourceProtobox: if _, err := sbObj.GetProtoboxCollectionData(); err != nil { return nil, fmt.Errorf("failed to load protobox data: %w", err) } if err := sbObj.ParseProtoboxCollection(); err != nil { return nil, fmt.Errorf("failed to parse protobox data: %w", err) } case sandboxDiffSourceProfile: if _, err := sbObj.GetPlatformProfileData(); err != nil { return nil, fmt.Errorf("failed to load platform profile data: %w", err) } if err := sbObj.ParseSandboxProfile(); err != nil { return nil, fmt.Errorf("failed to parse platform profile data: %w", err) } default: return nil, fmt.Errorf("unsupported sandbox source %q", source) } return renderSandboxProfiles(sbObj, source) } func renderSandboxProfiles(sbObj *sandbox.Sandbox, source string) (map[string]string, error) { out := make(map[string]string, len(sbObj.Profiles)) limit := maxSandboxDiffNormalizedNodes if source == sandboxDiffSourceProfile { // 0 disables the per-operation node budget. The platform profile is // one large standalone document, unlike collection profile entries. limit = 0 } for idx, prof := range sbObj.Profiles { name := sandboxProfileDocumentName(source, prof, idx) formatted, diags, err := normalize.FormatCompilerSafeProfileWithDiagnostics( sbObj, prof, limit, maxSandboxDiffOutputBytes, ) if err != nil { return nil, fmt.Errorf("failed to render %s: %w", name, err) } if strings.TrimSpace(formatted) == "" { continue } if len(diags) > 0 { log.Warnf("%s/%s: %d sandbox operation(s) skipped due to budget limits", source, name, len(diags)) } if _, exists := out[name]; exists { name = uniqueSandboxProfileDocumentName(out, name, idx) } out[name] = formatted } return out, nil } func sandboxProfileDocumentName(source string, prof sandbox.Profile, idx int) string { if prof.Name != "" { return prof.Name } if source == sandboxDiffSourceProfile { return "platform" } return fmt.Sprintf("profile_%03d", idx) } func uniqueSandboxProfileDocumentName(existing map[string]string, name string, idx int) string { candidate := fmt.Sprintf("%s#%d", name, idx) for suffix := 2; ; suffix++ { if _, exists := existing[candidate]; !exists { return candidate } candidate = fmt.Sprintf("%s#%d.%d", name, idx, suffix) } } func isSandboxSourceUnavailable(err error) bool { return errors.Is(err, sandbox.ErrSandboxSourceUnavailable) } func buildSandboxDiffFixupMap(kernel *macho.File) (map[uint64]uint64, error) { fixups := make(map[uint64]uint64) if !kernel.HasFixups() { return fixups, nil } dcf, err := kernel.DyldChainedFixups() if err != nil { return nil, err } for _, start := range dcf.Starts { if start.PageStarts == nil { continue } for _, fixup := range start.Fixups { if rebase, ok := fixup.(fixupchains.Rebase); ok { fixups[rebase.Raw()] = uint64(rebase.Offset()) + kernel.GetBaseAddress() } } } return fixups, nil }