Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/45327-detail-query-error
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Fixed a macOS detail query error caused by app bundles (such as Apple's XProtect) that declare an executable in their Info.plist but ship no binary at that path; the executable SHA256 is now left empty instead of failing the query.
12 changes: 11 additions & 1 deletion orbit/pkg/table/executable_hashes/executable_hashes.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,17 @@ func computeFileSHA256(filePath string) (string, error) {
}
f, err := os.Open(filePath)
if err != nil {
return "", fmt.Errorf("couldn't open filepath: %w", err)
// The executable named in the bundle's Info.plist may not exist on disk — e.g.
// Apple system bundles like XProtect.bundle declare a CFBundleExecutable but ship
// no binary at that path. Don't fail the whole table generation (which aborts the
// detail query for the host); log at debug and return an empty hash, matching the
// empty-path behavior above. Any other open error (permission denied, transient
// I/O, etc.) is unexpected, so propagate it rather than silently masking it.
if errors.Is(err, os.ErrNotExist) {
log.Debug().Err(err).Str("path", filePath).Msg("executable not found on disk, returning empty hash")
return "", nil
}
return "", fmt.Errorf("opening executable to compute sha256: %w", err)
}
defer f.Close()

Expand Down
125 changes: 125 additions & 0 deletions orbit/pkg/table/executable_hashes/executable_hashes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,131 @@ func TestGenerateWithExactPath(t *testing.T) {
}
}

// TestGenerateWithExactPathMissingExecutable reproduces issue #45327: some app
// bundles (e.g. Apple's XProtect.bundle) declare a CFBundleExecutable in their
// Info.plist but ship no binary at that path. Generating the table must not fail.
func TestGenerateWithExactPathMissingExecutable(t *testing.T) {
dir := t.TempDir()

bundlePath := filepath.Join(dir, "XProtect.bundle")
contentsDir := filepath.Join(bundlePath, "Contents")
macosDir := filepath.Join(contentsDir, "MacOS")
require.NoError(t, os.MkdirAll(macosDir, 0o755))

// Valid Info.plist that names an executable, but we intentionally do NOT
// create Contents/MacOS/XProtect.
infoPlistPath := filepath.Join(contentsDir, "Info.plist")
infoPlistContent := `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>XProtect</string>
</dict>
</plist>`
require.NoError(t, os.WriteFile(infoPlistPath, []byte(infoPlistContent), 0o644))

rows, err := Generate(t.Context(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
colPath: {
Constraints: []table.Constraint{{
Expression: bundlePath,
Operator: table.OperatorEquals,
}},
},
},
})
require.NoError(t, err)
require.Len(t, rows, 1)
require.Equal(t, bundlePath, rows[0][colPath])
require.Equal(t, filepath.Join(contentsDir, "MacOS", "XProtect"), rows[0][colExecPath])
require.Empty(t, rows[0][colExecHash])
}

// TestGenerateWithWildcardPartialMissingExecutables ensures a single bundle whose
// executable is missing does not abort hashing of the rest of the wildcard batch.
func TestGenerateWithWildcardPartialMissingExecutables(t *testing.T) {
dir := t.TempDir()

testBundles := map[string]struct {
executableName string
content []byte
createExec bool
}{
"Good.app": {"Good", []byte("content of good"), true},
"Missing.app": {"Missing", nil, false},
}

expectedHashByBundlePath := make(map[string]string)
expectedExecPathByBundlePath := make(map[string]string)

for bundleName, bundleInfo := range testBundles {
bundlePath := filepath.Join(dir, bundleName)
contentsDir := filepath.Join(bundlePath, "Contents")
macosDir := filepath.Join(contentsDir, "MacOS")
require.NoError(t, os.MkdirAll(macosDir, 0o755))

infoPlistPath := filepath.Join(contentsDir, "Info.plist")
infoPlistContent := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>%s</string>
</dict>
</plist>`, bundleInfo.executableName)
require.NoError(t, os.WriteFile(infoPlistPath, []byte(infoPlistContent), 0o644))

execPath := filepath.Join(macosDir, bundleInfo.executableName)
expectedExecPathByBundlePath[bundlePath] = execPath

if bundleInfo.createExec {
require.NoError(t, os.WriteFile(execPath, bundleInfo.content, 0o644))
h := sha256.New()
h.Write(bundleInfo.content)
expectedHashByBundlePath[bundlePath] = hex.EncodeToString(h.Sum(nil))
} else {
// Missing executable -> empty hash, but still a row.
expectedHashByBundlePath[bundlePath] = ""
}
}

rows, err := Generate(t.Context(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
colPath: {
Constraints: []table.Constraint{{
Expression: filepath.Join(dir, "%.app"),
Operator: table.OperatorLike,
}},
},
},
})
require.NoError(t, err)
require.Len(t, rows, 2)

got := make(map[string]fileInfo, 2)
for _, row := range rows {
got[row[colPath]] = fileInfo{
Path: row[colPath],
ExecPath: row[colExecPath],
ExecSha256: row[colExecHash],
}
}

for bundlePath, expectedHash := range expectedHashByBundlePath {
require.Contains(t, got, bundlePath)
info := got[bundlePath]
require.Equal(t, expectedExecPathByBundlePath[bundlePath], info.ExecPath)
require.Equal(t, expectedHash, info.ExecSha256)
}
}

func TestComputeFileSHA256MissingFile(t *testing.T) {
hash, err := computeFileSHA256(filepath.Join(t.TempDir(), "does-not-exist"))
require.NoError(t, err)
require.Empty(t, hash)
}

func TestGenerateWithWildcard(t *testing.T) {
dir := t.TempDir()
defer os.RemoveAll(dir)
Expand Down
Loading