Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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.
8 changes: 7 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,13 @@ 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); warn and return an empty hash, matching the
// empty-path behavior above.
log.Warn().Err(err).Str("path", filePath).Msg("couldn't open executable to compute sha256, returning empty hash")
return "", nil
Comment thread
dantecatalfamo marked this conversation as resolved.
Outdated
}
defer f.Close()

Expand Down
124 changes: 124 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,130 @@ 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")
require.NoError(t, os.MkdirAll(contentsDir, 0o755))
Comment thread
Copilot marked this conversation as resolved.
Outdated

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