Fixed a bug where the fleetd executable_hashes table failed to compute hashes for app bundles with emoji characters in their names (#41638)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #41328

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

## fleetd/orbit/Fleet Desktop

- [x] Verified compatibility with the latest released version of Fleet
(see [Must rule]
- [x] If the change applies to only one platform, confirmed that
`runtime.GOOS` is used as needed to isolate changes
- [x] Verified auto-update works from the released version of component
to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Fixed an issue where executable hashes failed to compute for macOS app
bundles with emoji or other Unicode characters in executable names,
improving bundle detection and integrity checks.

* **Tests**
* Added comprehensive tests to ensure correct handling of Unicode escape
sequences and emoji in bundle names and executables.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky
2026-03-13 13:26:19 -05:00
committed by GitHub
parent fe1e4d295b
commit fa30866e40
5 changed files with 195 additions and 38 deletions
@@ -0,0 +1 @@
* Fixed a bug where the fleetd `executable_hashes` table failed to compute hashes for app bundles with emoji characters in their names.
@@ -15,6 +15,7 @@ import (
"path/filepath"
"strings"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/osquery/osquery-go/plugin/table"
"github.com/rs/zerolog/log"
)
@@ -146,5 +147,9 @@ func getExecutablePath(ctx context.Context, path string) string {
return ""
}
// The macOS `defaults read` command encodes supplementary Unicode characters (such as emoji) as
// \uXXXX escape sequences using UTF-16 surrogate pairs.
executableName = fleet.DecodeUnicodeEscapes(executableName)
return filepath.Join(path, "/Contents/MacOS/", executableName)
}
@@ -3,7 +3,6 @@
package executable_hashes
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
@@ -16,52 +15,70 @@ import (
)
func TestGenerateWithExactPath(t *testing.T) {
dir := t.TempDir()
defer os.RemoveAll(dir)
tests := []struct {
name string
bundleName string
executableName string
content []byte
}{
{
name: "ASCII app name",
bundleName: "Test.app",
executableName: "Test",
content: []byte("test file content for hashing"),
},
{
name: "emoji app name",
bundleName: "🖨️ Printer.app",
executableName: "🖨️ Printer",
content: []byte("emoji executable content"),
},
}
// Create a macOS app bundle structure
bundlePath := filepath.Join(dir, "Test.app")
contentsDir := filepath.Join(bundlePath, "Contents")
macosDir := filepath.Join(contentsDir, "MacOS")
require.NoError(t, os.MkdirAll(macosDir, 0o755))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
execName := "Test"
// Create Info.plist with CFBundleExecutable key
infoPlistPath := filepath.Join(contentsDir, "Info.plist")
infoPlistContent := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
bundlePath := filepath.Join(dir, tt.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>`, execName)
require.NoError(t, os.WriteFile(infoPlistPath, []byte(infoPlistContent), 0o644))
</plist>`, tt.executableName)
require.NoError(t, os.WriteFile(infoPlistPath, []byte(infoPlistContent), 0o644))
// Create the actual executable binary in Contents/MacOS/
execPath := filepath.Join(macosDir, execName)
content := []byte("test file content for hashing")
require.NoError(t, os.WriteFile(execPath, content, 0o644))
execPath := filepath.Join(macosDir, tt.executableName)
require.NoError(t, os.WriteFile(execPath, tt.content, 0o644))
h := sha256.New()
h.Write(content)
expectedHash := hex.EncodeToString(h.Sum(nil))
h := sha256.New()
h.Write(tt.content)
expectedHash := hex.EncodeToString(h.Sum(nil))
rows, err := Generate(context.Background(), 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, execPath, rows[0][colExecPath])
require.Equal(t, expectedHash, rows[0][colExecHash])
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, execPath, rows[0][colExecPath])
require.Equal(t, expectedHash, rows[0][colExecHash])
})
}
}
func TestGenerateWithWildcard(t *testing.T) {
@@ -110,7 +127,7 @@ func TestGenerateWithWildcard(t *testing.T) {
expectedExecPathByBundlePath[bundlePath] = execPath
}
rows, err := Generate(context.Background(), table.QueryContext{
rows, err := Generate(t.Context(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
colPath: {
Constraints: []table.Constraint{{
@@ -123,7 +140,7 @@ func TestGenerateWithWildcard(t *testing.T) {
require.NoError(t, err)
require.Len(t, rows, 2)
serviceRows, err := Generate(context.Background(), table.QueryContext{
serviceRows, err := Generate(t.Context(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
colPath: {
Constraints: []table.Constraint{{