Files
ipsw/internal/diff/sandbox.go
2026-06-04 15:23:21 -06:00

204 lines
5.9 KiB
Go

//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
}