diff --git a/cmd/ipsw/cmd/extract.go b/cmd/ipsw/cmd/extract.go index ec83b846c..d08a2876a 100644 --- a/cmd/ipsw/cmd/extract.go +++ b/cmd/ipsw/cmd/extract.go @@ -48,27 +48,6 @@ func isURL(str string) bool { return err == nil && u.Scheme != "" && u.Host != "" } -func extractFromDMG(ipswPath, dmgPath, destPath string, pattern *regexp.Regexp) error { - // check if filesystem DMG already exists (due to previous mount command) - if _, err := os.Stat(dmgPath); os.IsNotExist(err) { - dmgs, err := utils.Unzip(ipswPath, "", func(f *zip.File) bool { - return strings.EqualFold(filepath.Base(f.Name), dmgPath) - }) - if err != nil { - return fmt.Errorf("failed to extract %s from IPSW: %v", dmgPath, err) - } - if len(dmgs) == 0 { - return fmt.Errorf("failed to find %s in IPSW", dmgPath) - } - defer os.Remove(dmgs[0]) - } - if err := utils.ExtractFromDMG(dmgPath, destPath, pattern); err != nil { - return fmt.Errorf("failed to extract files matching pattern: %v", err) - } - - return nil -} - func init() { rootCmd.AddCommand(extractCmd) @@ -346,17 +325,17 @@ var extractCmd = &cobra.Command{ if viper.GetBool("extract.files") { // SEARCH THE DMGs if appOS, err := i.GetAppOsDmg(); err == nil { - if err := extractFromDMG(ipswPath, appOS, destPath, patternRE); err != nil { + if err := utils.ExtractFromDMG(ipswPath, appOS, destPath, patternRE); err != nil { return fmt.Errorf("failed to extract files from AppOS %s: %v", appOS, err) } } if systemOS, err := i.GetSystemOsDmg(); err == nil { - if err := extractFromDMG(ipswPath, systemOS, destPath, patternRE); err != nil { + if err := utils.ExtractFromDMG(ipswPath, systemOS, destPath, patternRE); err != nil { return fmt.Errorf("failed to extract files from SystemOS %s: %v", systemOS, err) } } if fsOS, err := i.GetFileSystemOsDmg(); err == nil { - if err := extractFromDMG(ipswPath, fsOS, destPath, patternRE); err != nil { + if err := utils.ExtractFromDMG(ipswPath, fsOS, destPath, patternRE); err != nil { return fmt.Errorf("failed to extract files from filesystem %s: %v", fsOS, err) } } diff --git a/internal/diff/diff.go b/internal/diff/diff.go index 054fcaa01..f0463cd69 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -1,3 +1,4 @@ +// Package diff provides a way to diff two ipsws package diff import ( @@ -7,8 +8,10 @@ import ( "fmt" "os" "path/filepath" + "regexp" "sort" "strings" + "sync" "text/tabwriter" "time" @@ -22,18 +25,32 @@ import ( "github.com/blacktop/ipsw/pkg/kernelcache" ) +const ( + systemOsDmg = "sys" + appOsDmg = "app" + fileSystemDmg = "fs" +) + type kernel struct { Path string Version *kernelcache.Version Kexts []string } +type mount struct { + DmgPath string + MountPath string + IsMounted bool +} + +// Context is the context for the diff type Context struct { IPSWPath string Info *info.Info Version string Build string Folder string + Mount map[string]mount SystemOsDmgPath string MountPath string IsMounted bool @@ -41,8 +58,11 @@ type Context struct { DSC string Webkit string KDK string + + mu *sync.Mutex } +// Diff is the diff type Diff struct { Title string @@ -57,6 +77,7 @@ type Diff struct { Removed string Updated string } + Launchd string tmpDir string } @@ -68,9 +89,11 @@ func New(title, ipswOld, ipswNew string, kdks []string) *Diff { Title: title, Old: Context{ IPSWPath: ipswOld, + Mount: make(map[string]mount), }, New: Context{ IPSWPath: ipswNew, + Mount: make(map[string]mount), }, } } @@ -78,10 +101,12 @@ func New(title, ipswOld, ipswNew string, kdks []string) *Diff { Title: title, Old: Context{ IPSWPath: ipswOld, + Mount: make(map[string]mount), KDK: kdks[0], }, New: Context{ IPSWPath: ipswNew, + Mount: make(map[string]mount), KDK: kdks[1], }, } @@ -166,6 +191,11 @@ func (d *Diff) Diff() (err error) { return err } + log.Info("Diffing launchd PLIST") + if err := d.parseLaunchdPlists(); err != nil { + return fmt.Errorf("failed to parse launchd config plists: %v", err) + } + log.Info("Diffing ENTITLEMENTS") d.Ents, err = d.parseEntitlements() if err != nil { @@ -459,3 +489,54 @@ func (d *Diff) parseEntitlements() (string, error) { DiffTool: "git", }) } + +func (d *Diff) parseLaunchdPlists() error { + oldFsDMG, err := d.Old.Info.GetFileSystemOsDmg() + if err != nil { + return fmt.Errorf("failed to get 'Old' File System DMG: %v", err) + } + if err := utils.ExtractFromDMG(d.Old.IPSWPath, oldFsDMG, filepath.Join(d.tmpDir, "old"), regexp.MustCompile(`.*/sbin/launchd$`)); err != nil { + return err + } + m, err := macho.Open(filepath.Join(d.tmpDir, "old/sbin/launchd")) + if err != nil { + return err + } + defer m.Close() + + oldData, err := m.Section("__TEXT", "__config").Data() + if err != nil { + return err + } + + newFsDMG, err := d.New.Info.GetFileSystemOsDmg() + if err != nil { + return fmt.Errorf("failed to get 'New' File System DMG: %v", err) + } + if err := utils.ExtractFromDMG(d.New.IPSWPath, newFsDMG, filepath.Join(d.tmpDir, "new"), regexp.MustCompile(`.*/sbin/launchd$`)); err != nil { + return err + } + m2, err := macho.Open(filepath.Join(d.tmpDir, "new/sbin/launchd")) + if err != nil { + return err + } + defer m2.Close() + + newData, err := m.Section("__TEXT", "__config").Data() + if err != nil { + return err + } + + out, err := utils.GitDiff( + string(oldData)+"\n", + string(newData)+"\n", + &utils.GitDiffConfig{Color: false, Tool: "git"}) + if err != nil { + return err + } + if len(out) > 0 { + d.Launchd = "```diff\n" + out + "\n```" + } + + return nil +} diff --git a/internal/diff/format.go b/internal/diff/format.go index a328bb8b9..87a1cebd6 100644 --- a/internal/diff/format.go +++ b/internal/diff/format.go @@ -74,6 +74,11 @@ const diffMarkdownTemplate = ` {{ .Ents | noescape }} +{{ if .Launchd }} +## launchd Config +{{ .Launchd | noescape }} +{{end -}} + ## DSC ### WebKit diff --git a/internal/utils/macos.go b/internal/utils/macos.go index b0d8be25b..52169b968 100644 --- a/internal/utils/macos.go +++ b/internal/utils/macos.go @@ -1,6 +1,7 @@ package utils import ( + "archive/zip" "bytes" "encoding/binary" "errors" @@ -12,6 +13,7 @@ import ( "regexp" "runtime" "strings" + "time" "github.com/apex/log" "github.com/blacktop/go-plist" @@ -382,69 +384,70 @@ func Mount(image, mountPoint string) error { } return nil + } - } else if runtime.GOOS == "linux" { - cmd := exec.Command("apfs-fuse", image, mountPoint) + if _, err := exec.LookPath("apfs-fuse"); err != nil { + return fmt.Errorf("utils.Mount: apfs-fuse not found (required on non-darwin systems): %v", err) + } - out, err := cmd.CombinedOutput() - if err != nil { - if strings.Contains(string(out), "hdiutil: mount failed - Resource busy") { - return ErrMountResourceBusy - } - return fmt.Errorf("%v: %s", err, out) + cmd := exec.Command("apfs-fuse", image, mountPoint) + + out, err := cmd.CombinedOutput() + if err != nil { + if strings.Contains(string(out), "hdiutil: mount failed - Resource busy") { + return ErrMountResourceBusy } - - return nil + return fmt.Errorf("%v: %s", err, out) } return nil } -func IsAlreadyMounted(image string) (string, bool, error) { - info, err := MountInfo() - if err != nil { - return "", false, err - } - for _, i := range info.Images { - if strings.Contains(i.ImagePath, image) { - for _, entry := range i.SystemEntities { - if entry.MountPoint != "" { - return entry.MountPoint, true, nil +func IsAlreadyMounted(image, mountPoint string) (string, bool, error) { + if runtime.GOOS == "darwin" { + info, err := MountInfo() + if err != nil { + return "", false, err + } + for _, i := range info.Images { + if strings.Contains(i.ImagePath, image) { + for _, entry := range i.SystemEntities { + if entry.MountPoint != "" { + return entry.MountPoint, true, nil + } } + return "", true, nil } - return "", true, nil + } + } else if runtime.GOOS == "linux" { + if _, err := os.Stat(filepath.Join(mountPoint, "root")); !os.IsNotExist(err) { + return mountPoint, true, nil } } return "", false, nil } func MountFS(image string) (string, bool, error) { - var mountPoint string + mountPoint := fmt.Sprintf("/tmp/%s.mount", filepath.Base(image)) + if runtime.GOOS == "darwin" { - mountPoint = fmt.Sprintf("/tmp/%s.mount", filepath.Base(image)) // check if already mounted - if prevMountPoint, mounted, err := IsAlreadyMounted(image); mounted && err == nil { + if prevMountPoint, mounted, err := IsAlreadyMounted(image, mountPoint); mounted && err == nil { if prevMountPoint != "" { mountPoint = prevMountPoint } return mountPoint, true, nil } } else { - if _, ok := os.LookupEnv("IPSW_IN_DOCKER"); ok { - // Create in-docker mount point - os.MkdirAll("/data", 0750) - mountPoint = "/mnt" - } else { - // Create temporary non-darwin mount point - mountPoint = image + "_temp_mount" - if err := os.Mkdir(mountPoint, 0750); err != nil { - return "", false, fmt.Errorf("failed to create temporary mount point %s: %v", mountPoint, err) - } + if err := os.Mkdir(mountPoint, 0750); err != nil { + return "", false, fmt.Errorf("failed to create temporary mount point %s: %v", mountPoint, err) } } + if err := Mount(image, mountPoint); err != nil { return "", false, fmt.Errorf("failed to mount %s: %v", image, err) } + return mountPoint, false, nil } @@ -459,20 +462,16 @@ func Unmount(mountPoint string, force bool) error { cmd = exec.Command("hdiutil", "detach", mountPoint) } - err := cmd.Run() - if err != nil { + if err := cmd.Run(); err != nil { var edetail string if strings.Contains(err.Error(), "exit status 16") { edetail = " (Resource busy)" } return fmt.Errorf("failed to unmount %s%s: %v", mountPoint, edetail, err) } - } else if runtime.GOOS == "linux" { cmd := exec.Command("umount", mountPoint) - - err := cmd.Run() - if err != nil { + if err := cmd.Run(); err != nil { return fmt.Errorf("failed to unmount %s: %v", mountPoint, err) } } @@ -524,7 +523,20 @@ func MountInfo() (*HdiUtilInfo, error) { return nil, fmt.Errorf("only supported on macOS") } -func ExtractFromDMG(dmgPath, destPath string, pattern *regexp.Regexp) error { +func ExtractFromDMG(ipswPath, dmgPath, destPath string, pattern *regexp.Regexp) error { + // check if filesystem DMG already exists (due to previous mount command) + if _, err := os.Stat(dmgPath); os.IsNotExist(err) { + dmgs, err := Unzip(ipswPath, "", func(f *zip.File) bool { + return strings.EqualFold(filepath.Base(f.Name), dmgPath) + }) + if err != nil { + return fmt.Errorf("failed to extract %s from IPSW: %v", dmgPath, err) + } + if len(dmgs) == 0 { + return fmt.Errorf("failed to find %s in IPSW", dmgPath) + } + defer os.Remove(dmgs[0]) + } Indent(log.Info, 2)(fmt.Sprintf("Mounting DMG %s", dmgPath)) mountPoint, alreadyMounted, err := MountFS(dmgPath) @@ -536,8 +548,10 @@ func ExtractFromDMG(dmgPath, destPath string, pattern *regexp.Regexp) error { } else { defer func() { Indent(log.Debug, 2)(fmt.Sprintf("Unmounting %s", dmgPath)) - if err := Unmount(mountPoint, false); err != nil { - log.Errorf("failed to unmount DMG at %s: %v", dmgPath, err) + if err := Retry(3, 2*time.Second, func() error { + return Unmount(mountPoint, false) + }); err != nil { + log.Errorf("failed to unmount DMG %s at %s: %v", dmgPath, mountPoint, err) } }() } diff --git a/pkg/dyld/extract.go b/pkg/dyld/extract.go index b4e7b8662..94d040e8a 100644 --- a/pkg/dyld/extract.go +++ b/pkg/dyld/extract.go @@ -77,15 +77,9 @@ func ExtractFromDMG(i *info.Info, dmgPath, destPath string, arches []string) err return fmt.Errorf("failed to create destination directory %s: %v", destPath, err) } } else { - // Create temporary mount point if err := os.MkdirAll(destPath, 0750); err != nil { return fmt.Errorf("failed to create destination directory %s: %v", destPath, err) } - if err := os.MkdirAll(mountPoint, 0750); err != nil { - return fmt.Errorf("failed to create temporary mount point %s: %v", mountPoint, err) - } else { - defer os.RemoveAll(mountPoint) - } } }