From 0e7953320af7d9cbcecc855122ac14d77720bdd2 Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Thu, 11 Jun 2026 16:14:31 -0400 Subject: [PATCH 1/3] handle missing macOS bundle executable in executable_hashes table --- changes/45327-detail-query-error | 1 + .../executable_hashes/executable_hashes.go | 8 +- .../executable_hashes_test.go | 124 ++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 changes/45327-detail-query-error diff --git a/changes/45327-detail-query-error b/changes/45327-detail-query-error new file mode 100644 index 00000000000..91399fe7603 --- /dev/null +++ b/changes/45327-detail-query-error @@ -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. diff --git a/orbit/pkg/table/executable_hashes/executable_hashes.go b/orbit/pkg/table/executable_hashes/executable_hashes.go index eedb13c7132..457af740d51 100644 --- a/orbit/pkg/table/executable_hashes/executable_hashes.go +++ b/orbit/pkg/table/executable_hashes/executable_hashes.go @@ -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 } defer f.Close() diff --git a/orbit/pkg/table/executable_hashes/executable_hashes_test.go b/orbit/pkg/table/executable_hashes/executable_hashes_test.go index 2da31364b18..533b8559f89 100644 --- a/orbit/pkg/table/executable_hashes/executable_hashes_test.go +++ b/orbit/pkg/table/executable_hashes/executable_hashes_test.go @@ -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)) + + // Valid Info.plist that names an executable, but we intentionally do NOT + // create Contents/MacOS/XProtect. + infoPlistPath := filepath.Join(contentsDir, "Info.plist") + infoPlistContent := ` + + + + CFBundleExecutable + XProtect + +` + 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(` + + + + CFBundleExecutable + %s + +`, 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) From 966740b8b362ee8f9ab5b00bcf7b87996fe4bd94 Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:30:22 -0400 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- orbit/pkg/table/executable_hashes/executable_hashes_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/orbit/pkg/table/executable_hashes/executable_hashes_test.go b/orbit/pkg/table/executable_hashes/executable_hashes_test.go index 533b8559f89..bd6c4ea11be 100644 --- a/orbit/pkg/table/executable_hashes/executable_hashes_test.go +++ b/orbit/pkg/table/executable_hashes/executable_hashes_test.go @@ -89,7 +89,8 @@ func TestGenerateWithExactPathMissingExecutable(t *testing.T) { bundlePath := filepath.Join(dir, "XProtect.bundle") contentsDir := filepath.Join(bundlePath, "Contents") - require.NoError(t, os.MkdirAll(contentsDir, 0o755)) + 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. From 5ca75cb00be62fa6a8ff7b19c7bf44a5574ef671 Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Fri, 12 Jun 2026 15:30:43 -0400 Subject: [PATCH 3/3] Don't consume all errors, look for file not exist specifically --- .../pkg/table/executable_hashes/executable_hashes.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/orbit/pkg/table/executable_hashes/executable_hashes.go b/orbit/pkg/table/executable_hashes/executable_hashes.go index 457af740d51..e0818405076 100644 --- a/orbit/pkg/table/executable_hashes/executable_hashes.go +++ b/orbit/pkg/table/executable_hashes/executable_hashes.go @@ -124,10 +124,14 @@ func computeFileSHA256(filePath string) (string, error) { // 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 + // 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()