Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i/builtin: support desktop-file-ids in desktop files rule generation #14444

Merged
4 changes: 1 addition & 3 deletions interfaces/builtin/desktop_legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
package builtin

import (
"strings"

"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/apparmor"
)
Expand Down Expand Up @@ -395,7 +393,7 @@ func (iface *desktopLegacyInterface) AppArmorConnectedPlug(spec *apparmor.Specif
// interfaces (like desktop-launch), so they are added here with the minimum
// priority, while those other, more privileged, interfaces will add an empty
// string with a bigger privilege value.
desktopSnippet := strings.Join(getDesktopFileRules(plug.Snap()), "\n")
desktopSnippet := getDesktopFileRules(plug.Snap())
spec.AddPrioritizedSnippet(desktopSnippet, prioritizedSnippetDesktopFileAccess, desktopLegacyAndUnity7Priority)

return nil
Expand Down
4 changes: 2 additions & 2 deletions interfaces/builtin/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,6 @@ func MockApparmorGenerateAAREExclusionPatterns(fn func(excludePatterns []string,
return testutil.Mock(&apparmorGenerateAAREExclusionPatterns, fn)
}

func MockDesktopFilesFromMount(fn func(s *snap.Info) ([]string, error)) (restore func()) {
return testutil.Mock(&desktopFilesFromMount, fn)
func MockDesktopFilesFromInstalledSnap(fn func(s *snap.Info) ([]string, error)) (restore func()) {
return testutil.Mock(&desktopFilesFromInstalledSnap, fn)
}
2 changes: 1 addition & 1 deletion interfaces/builtin/unity7.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ func (iface *unity7Interface) AppArmorConnectedPlug(spec *apparmor.Specification
// interfaces (like desktop-launch), so they are added here with the minimum
// priority, while those other, more privileged, interfaces will add an empty
// string with a bigger privilege value.
desktopSnippet := strings.Join(getDesktopFileRules(plug.Snap()), "\n")
desktopSnippet := getDesktopFileRules(plug.Snap())
spec.AddPrioritizedSnippet(desktopSnippet, prioritizedSnippetDesktopFileAccess, desktopLegacyAndUnity7Priority)
return nil
}
Expand Down
91 changes: 48 additions & 43 deletions interfaces/builtin/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,26 +79,27 @@ func verifySlotPathAttribute(slotRef *interfaces.SlotRef, attrs interfaces.Attre
return cleanPath, nil
}

func getDesktopFileRulesFallback() []string {
return []string{
"# Support applications which use the unity messaging menu, xdg-mime, etc",
"# This leaks the names of snaps with desktop files",
fmt.Sprintf("%s/ r,", dirs.SnapDesktopFilesDir),
"# Allowing reading only our desktop files (required by (at least) the unity",
"# messaging menu).",
"# parallel-installs: this leaks read access to desktop files owned by keyed",
"# instances of @{SNAP_NAME} to @{SNAP_NAME} snap",
fmt.Sprintf("%s/@{SNAP_INSTANCE_DESKTOP}_*.desktop r,", dirs.SnapDesktopFilesDir),
"# Explicitly deny access to other snap's desktop files",
fmt.Sprintf("deny %s/@{SNAP_INSTANCE_DESKTOP}[^_.]*.desktop r,", dirs.SnapDesktopFilesDir),
// XXX: Do we need to generate extensive deny rules for the fallback too?
}
func getDesktopFileRulesFallback() string {
const template = `
# Support applications which use the unity messaging menu, xdg-mime, etc
# This leaks the names of snaps with desktop files
%[1]s/ r,
# Allowing reading only our desktop files (required by (at least) the unity
# messaging menu).
# parallel-installs: this leaks read access to desktop files owned by keyed
# instances of @{SNAP_NAME} to @{SNAP_NAME} snap
%[1]s/@{SNAP_INSTANCE_DESKTOP}_*.desktop r,
# Explicitly deny access to other snap's desktop files
deny %[1]s/@{SNAP_INSTANCE_DESKTOP}[^_.]*.desktop r,
`
// XXX: Do we need to generate extensive deny rules for the fallback too?
return fmt.Sprintf(template[1:], dirs.SnapDesktopFilesDir)
}

var apparmorGenerateAAREExclusionPatterns = apparmor.GenerateAAREExclusionPatterns
var desktopFilesFromMount = func(s *snap.Info) ([]string, error) {
opts := snap.DesktopFilesFromMountOptions{MangleFileNames: true}
return s.DesktopFilesFromMount(opts)
var desktopFilesFromInstalledSnap = func(s *snap.Info) ([]string, error) {
opts := snap.DesktopFilesFromInstalledSnapOptions{MangleFileNames: true}
return s.DesktopFilesFromInstalledSnap(opts)
}

// getDesktopFileRules generates snippet rules for allowing access to the
Expand All @@ -107,57 +108,62 @@ var desktopFilesFromMount = func(s *snap.Info) ([]string, error) {
// to read all the desktop files in the dir, causing excessive noise. (LP: #1868051)
//
// The snap must be mounted.
func getDesktopFileRules(s *snap.Info) []string {
baseDir := dirs.SnapDesktopFilesDir
func getDesktopFileRules(s *snap.Info) string {
const template = `
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this could use strings.Builder as well now. Not essential, but give it a try locally, and if the changes look good maybe it's worth pushing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this what you had in mind?

diff --git a/interfaces/builtin/utils.go b/interfaces/builtin/utils.go
index 026f7668f5..2475d2f7f1 100644
--- a/interfaces/builtin/utils.go
+++ b/interfaces/builtin/utils.go
@@ -109,26 +109,18 @@ var desktopFilesFromInstalledSnap = func(s *snap.Info) ([]string, error) {
 //
 // The snap must be mounted.
 func getDesktopFileRules(s *snap.Info) string {
-       const template = `
-# Support applications which use the unity messaging menu, xdg-mime, etc
-# This leaks the names of snaps with desktop files
-%s/ r,
+       var b strings.Builder
 
-# Allow rules:
-%s
-
-# Deny rules:
-%s
-`
+       b.WriteString("# Support applications which use the unity messaging menu, xdg-mime, etc\n")
+       b.WriteString("# This leaks the names of snaps with desktop files\n")
+       b.WriteString(fmt.Sprintf("%s/ r,\n", dirs.SnapDesktopFilesDir))
 
        // Generate allow rules
-       allowRules := `
-# Allowing reading only our desktop files (required by (at least) the unity
-# messaging menu).
-# parallel-installs: this leaks read access to desktop files owned by keyed
-# instances of @{SNAP_NAME} to @{SNAP_NAME} snap
-`
-       allowRules += fmt.Sprintf("%s/@{SNAP_INSTANCE_DESKTOP}_*.desktop r,\n", dirs.SnapDesktopFilesDir)
+       b.WriteString("# Allowing reading only our desktop files (required by (at least) the unity\n")
+       b.WriteString("# messaging menu).\n")
+       b.WriteString("# parallel-installs: this leaks read access to desktop files owned by keyed\n")
+       b.WriteString("# instances of @{SNAP_NAME} to @{SNAP_NAME} snap\n")
+       b.WriteString(fmt.Sprintf("%s/@{SNAP_INSTANCE_DESKTOP}_*.desktop r,\n", dirs.SnapDesktopFilesDir))
        // For allow rules let's be more defensive and not depend on desktop files
        // shipped by the snap like what is done below in the deny rules so that if
        // a snap figured out a way to trick the checks below it can only shoot
@@ -149,11 +141,11 @@ func getDesktopFileRules(s *snap.Info) string {
                        logger.Noticef("internal error: invalid desktop file ID %q found in snap %q: %v", desktopFileID, s.InstanceName(), err)
                        return getDesktopFileRulesFallback()
                }
-               allowRules += fmt.Sprintf("%s/%s r,\n", dirs.SnapDesktopFilesDir, desktopFileID+".desktop")
+               b.WriteString(fmt.Sprintf("%s/%s r,\n", dirs.SnapDesktopFilesDir, desktopFileID+".desktop"))
        }
 
        // Generate deny rules to suppress apparmor warnings
-       denyRules := "# Explicitly deny access to other snap's desktop files\n"
+       b.WriteString("# Explicitly deny access to other snap's desktop files\n")
        desktopFiles, err := desktopFilesFromInstalledSnap(s)
        if err != nil {
                logger.Noticef("failed to collect desktop files from snap %q: %v", s.InstanceName(), err)
@@ -188,9 +180,9 @@ func getDesktopFileRules(s *snap.Info) string {
                logger.Noticef("internal error: failed to generate deny rules for snap %q: %v", s.InstanceName(), err)
                return getDesktopFileRulesFallback()
        }
-       denyRules += excludeRules
+       b.WriteString(excludeRules)
 
-       return fmt.Sprintf(template, dirs.SnapDesktopFilesDir, allowRules[1:], denyRules)
+       return b.String()
 }
 
 // stringListAttribute returns a list of strings for the given attribute key if the attribute exists.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more less, builder implements Writer, so you can use fmt.Fprintf(&b, ...) where appropriate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

# Support applications which use the unity messaging menu, xdg-mime, etc
# This leaks the names of snaps with desktop files
%s/ r,

rules := []string{
"# Support applications which use the unity messaging menu, xdg-mime, etc",
"# This leaks the names of snaps with desktop files",
fmt.Sprintf("%s/ r,", baseDir),
}
# Allow rules:
%s

# Deny rules:
%s
`

// Generate allow rules
rules = append(rules,
"# Allowing reading only our desktop files (required by (at least) the unity",
"# messaging menu).",
"# parallel-installs: this leaks read access to desktop files owned by keyed",
"# instances of @{SNAP_NAME} to @{SNAP_NAME} snap",
fmt.Sprintf("%s/@{SNAP_INSTANCE_DESKTOP}_*.desktop r,", dirs.SnapDesktopFilesDir),
)
allowRules := `
# Allowing reading only our desktop files (required by (at least) the unity
# messaging menu).
# parallel-installs: this leaks read access to desktop files owned by keyed
# instances of @{SNAP_NAME} to @{SNAP_NAME} snap
`
allowRules += fmt.Sprintf("%s/@{SNAP_INSTANCE_DESKTOP}_*.desktop r,\n", dirs.SnapDesktopFilesDir)
// For allow rules let's be more defensive and not depend on desktop files
// shipped by the snap like what is done below in the deny rules so that if
// a snap figured out a way to trick the checks below it can only shoot
// itself in the foot and deny more stuff.
// Although, given the extensive use of ValidateNoAppArmorRegexp below this
// should never, but still it is better to play it safe with allow rules
// should never fail, but still it is better to play it safe with allow rules.
desktopFileIDs, err := s.DesktopPlugFileIDs()
if err != nil {
logger.Noticef("error: %v", err)
logger.Noticef("cannot list desktop plug file IDs: %v", err)
return getDesktopFileRulesFallback()
}
for _, desktopFileID := range desktopFileIDs {
// Validate IDs, This check should never be triggered because
// desktop-file-ids are already validated during install.
// But still it is better to play it safe and check AARE characters anyway.
if err := apparmor.ValidateNoAppArmorRegexp(desktopFileID); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wonder if this should perhaps be done in snap.Validate()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, Yes. But we don't know if there are snaps already in the store with desktop files with undesired names. adding the check in snap.Validate() could break existing installs (but maybe for such snaps, we should?).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we do any validation on the ids? I'm sure there are some checks that we can do just based on dbus rules, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do validation in the desktop interface's BeforePreparePlug

// https://specifications.freedesktop.org/desktop-entry-spec/latest/file-naming.html
// Desktop file id must be a valid D-Bus name:
// - A sequence of non-empty elements separated by dots
// - None of which starts with a digit
// - Each of which contains only characters from the set [A-Za-z0-9-_]
//
// XXX: dashes "-" are not recommended but supported, should they be removed?
// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names
var desktopFileIDRegexp = regexp.MustCompile(`^([A-Za-z_-][\w-]*)(\.[A-Za-z_-][\w-]*)*$`)
func (iface *desktopInterface) validateDesktopFileIDs(attribs interfaces.Attrer) error {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the check here to avoid depending on other parts working as expected and having the checks validate input standalone without any assumptions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer this is done in snap.Validate() since any existing snaps should be updated. However, if we are concerned about breaking things too much, we should ensure that review-tools implements similar validation logic for desktop-file-ids so we can avoid snaps being uploaded with anything that might try and abuse this (also recall this needs a snap declaration as well which adds additional protection too).

Copy link
Contributor Author

@ZeyadYasser ZeyadYasser Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No snaps on the store currently should have desktop-file-ids yet, I think we could add a check for them in snap.Validate() and fail hard there during install/pack. I was confused at first and thought this was for existing desktop file names as well.

Copy link
Contributor Author

@ZeyadYasser ZeyadYasser Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got confused a bit, I don't think we can this check to snap.Validate(). As Samuele mentioned the bad plug will not be there to validate since the snap plugs and slots are sanitized before getting validated so when we reach snap.Validate a bad desktop-file-ids attribute won't be there.

snap.BadInterfaces = make(map[string]string)
SanitizePlugsSlots(snap)

I don't see a clean way of force validating desktop-file-ids because they are sanitized (and disappear if bad) just before validation. Only thing left of them is an error message under info.BadInterfaces that is shown when running snap pack --check-skeleton.

$ snap pack --check-skeleton squashfs-root build
snap "hello" has bad plugs or slots: desktop (desktop-file-ids entry "org.he&&llo.Example" is not a valid D-Bus well-known name)

Maybe I am confused about what is required here, but I think adding validation on the container level snap.validateContainer() for desktop file names on pack/build as @bboozzoo suggested #14444 (comment) is a great idea + checks from review tools to prevent new revision with weird desktop file names.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it looks like an error here is unexpected and we should return early rather than simply ignore it. snap pack --check-skeleton raises an error with bad plugs/slots, but actual pack does not, which I think was intentional.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated that part to return and error instead of doing the fallback as this indicates something dangerous that the early validation and sanitization was bypassed by the snap.

logger.Noticef("error: %v", err)
logger.Noticef("internal error: invalid desktop file id %q found in snap %q which should have failed in BeforePreparePlug checks: %v", desktopFileID, s.InstanceName(), err)
ZeyadYasser marked this conversation as resolved.
Show resolved Hide resolved
return getDesktopFileRulesFallback()
}
rules = append(rules, fmt.Sprintf("%s/%s r,", baseDir, desktopFileID+".desktop"))
allowRules += fmt.Sprintf("%s/%s r,\n", dirs.SnapDesktopFilesDir, desktopFileID+".desktop")
}

// Generate deny rules to suppress apparmor warnings
desktopFiles, err := desktopFilesFromMount(s)
denyRules := "# Explicitly deny access to other snap's desktop files\n"
desktopFiles, err := desktopFilesFromInstalledSnap(s)
if err != nil {
logger.Noticef("error: %v", err)
logger.Noticef("failed to collect desktop files from snap %q: %v", s.InstanceName(), err)
return getDesktopFileRulesFallback()
}
if len(desktopFiles) == 0 {
// Nothing to do
return getDesktopFileRulesFallback()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we switch to the fallback in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to be consistent with the original implementation, which didn't care about existing desktop files.

func getDesktopFileRules(snapInstanceName string) []string {
baseDir := dirs.SnapDesktopFilesDir
rules := []string{
"# Support applications which use the unity messaging menu, xdg-mime, etc",
"# This leaks the names of snaps with desktop files",
fmt.Sprintf("%s/ r,", baseDir),
"# Allowing reading only our desktop files (required by (at least) the unity",
"# messaging menu).",
"# parallel-installs: this leaks read access to desktop files owned by keyed",
"# instances of @{SNAP_NAME} to @{SNAP_NAME} snap",
fmt.Sprintf("%s/@{SNAP_INSTANCE_DESKTOP}_*.desktop r,", baseDir),
"# Explicitly deny access to other snap's desktop files",
fmt.Sprintf("deny %s/@{SNAP_INSTANCE_DESKTOP}[^_.]*.desktop r,", baseDir),
}
for _, t := range aareExclusivePatterns(snapInstanceName) {
rules = append(rules, fmt.Sprintf("deny %s/%s r,", baseDir, t))
}
return rules
}

I know a snap depending on the undocumented behavior that unity7 and desktop-legacy gives it access to list desktop files should not be supported, but who knows what snap will break if that changes?

}
excludeOpts := &apparmor.AAREExclusionPatternsOptions{
Prefix: fmt.Sprintf("deny %s", baseDir),
Prefix: fmt.Sprintf("deny %s", dirs.SnapDesktopFilesDir),
Suffix: ".desktop r,",
}
excludePatterns := make([]string, 0, len(desktopFiles))
Expand All @@ -168,20 +174,19 @@ func getDesktopFileRules(s *snap.Info) []string {
// - Desktop file ids are validated to only contain non-AARE characters
// But still it is better to play it safe and check AARE characters anyway.
if err := apparmor.ValidateNoAppArmorRegexp(desktopFile); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snap.Validate() is also used when packing a snap, maybe it'd be useful to issue a warning if the there's anything wrong with the desktop files.

Hm I don't recall why we don't do any sanity checks of desktop files in validation code, and rather let them fail during install.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. A warning there wouldn't break existing snaps.

Hm I don't recall why we don't do any sanity checks of desktop files in validation code, and rather let them fail during install.

No idea, I recently landed changes there to validate their file types, but there weren't any other validations. I think without epochs we cannot do better on the container validation side.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A followup material, but we could look into tweaking snap.Validate() interface to pass additional parameter describing the scenario in which it is invoked, and in case of pack/build-time validation fail hard on some new checks we introduce.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A followup material, but we could look into tweaking snap.Validate() interface to pass additional parameter describing the scenario in which it is invoked, and in case of pack/build-time validation fail hard on some new checks we introduce.

This sounds like a great idea.

And we should also extend review-tools to implement similar checks against desktop file names etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed a ticket to track this idea https://warthogs.atlassian.net/browse/SNAPDENG-32333

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just have one comment, I think the desktop file names check should be on the container level, so I think snap.validateContainer() would be more appropriate to add the additional scenario parameter.

logger.Noticef("error: %v", err)
logger.Noticef("internal error: invalid desktop file name %q found in snap %q which should have been validated earlier: %v", desktopFile, s.InstanceName(), err)
ZeyadYasser marked this conversation as resolved.
Show resolved Hide resolved
return getDesktopFileRulesFallback()
}
excludePatterns = append(excludePatterns, "/"+strings.TrimSuffix(filepath.Base(desktopFile), ".desktop"))
}
excludeRules, err := apparmorGenerateAAREExclusionPatterns(excludePatterns, excludeOpts)
if err != nil {
logger.Noticef("error: %v", err)
logger.Noticef("internal error: failed to generate deny rules for snap %q: %v", s.InstanceName(), err)
return getDesktopFileRulesFallback()
}
rules = append(rules, "# Explicitly deny access to other snap's desktop files")
rules = append(rules, excludeRules)
denyRules += excludeRules

return rules
return fmt.Sprintf(template, dirs.SnapDesktopFilesDir, allowRules[1:], denyRules)
}

// stringListAttribute returns a list of strings for the given attribute key if the attribute exists.
Expand Down
29 changes: 25 additions & 4 deletions interfaces/builtin/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/snapcore/snapd/interfaces/apparmor"
"github.com/snapcore/snapd/interfaces/builtin"
"github.com/snapcore/snapd/interfaces/ifacetest"
"github.com/snapcore/snapd/logger"
apparmorutils "github.com/snapcore/snapd/sandbox/apparmor"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
Expand Down Expand Up @@ -305,7 +306,10 @@ func (s *desktopFileRulesBaseSuite) TestDesktopFileRulesNoDesktopFilesFallback(c
}

func (s *desktopFileRulesBaseSuite) TestDesktopFileRulesSnapMountErrorFallback(c *C) {
restore := builtin.MockDesktopFilesFromMount(func(s *snap.Info) ([]string, error) {
logbuf, restore := logger.MockLogger()
defer restore()

restore = builtin.MockDesktopFilesFromInstalledSnap(func(s *snap.Info) ([]string, error) {
return nil, errors.New("boom")
})
defer restore()
Expand All @@ -318,10 +322,15 @@ func (s *desktopFileRulesBaseSuite) TestDesktopFileRulesSnapMountErrorFallback(c
expectedRules: s.fallbackRules,
}
s.testDesktopFileRules(c, opts)

c.Check(logbuf.String(), testutil.Contains, `failed to collect desktop files from snap "some-snap": boom`)
}

func (s *desktopFileRulesBaseSuite) TestDesktopFileRulesAAREExclusionPatternsErrorFallback(c *C) {
restore := builtin.MockApparmorGenerateAAREExclusionPatterns(func(excludePatterns []string, opts *apparmorutils.AAREExclusionPatternsOptions) (string, error) {
logbuf, restore := logger.MockLogger()
defer restore()

restore = builtin.MockApparmorGenerateAAREExclusionPatterns(func(excludePatterns []string, opts *apparmorutils.AAREExclusionPatternsOptions) (string, error) {
return "", errors.New("boom")
})
defer restore()
Expand All @@ -334,6 +343,8 @@ func (s *desktopFileRulesBaseSuite) TestDesktopFileRulesAAREExclusionPatternsErr
expectedRules: s.fallbackRules,
}
s.testDesktopFileRules(c, opts)

c.Check(logbuf.String(), testutil.Contains, `internal error: failed to generate deny rules for snap "some-snap": boom`)
}

func (s *desktopFileRulesBaseSuite) TestDesktopFileRulesCommonSnapNameAndDesktopFileID(c *C) {
Expand Down Expand Up @@ -379,9 +390,12 @@ func (s *desktopFileRulesBaseSuite) TestDesktopFileRulesSanitizedDesktopFileName
}

func (s *desktopFileRulesBaseSuite) TestDesktopFileRulesBadDesktopFileName(c *C) {
logbuf, restore := logger.MockLogger()
defer restore()

// Stress the case where a snap file name skipped sanitization somehow
// This should never happen because snap.MangleDesktopFileName is called
restore := builtin.MockDesktopFilesFromMount(func(s *snap.Info) ([]string, error) {
restore = builtin.MockDesktopFilesFromInstalledSnap(func(s *snap.Info) ([]string, error) {
return []string{"foo**?$.desktop"}, nil
})
defer restore()
Expand All @@ -393,12 +407,17 @@ func (s *desktopFileRulesBaseSuite) TestDesktopFileRulesBadDesktopFileName(c *C)
expectedRules: s.fallbackRules,
}
s.testDesktopFileRules(c, opts)

c.Check(logbuf.String(), testutil.Contains, `internal error: invalid desktop file name "foo**?$.desktop" found in snap "some-snap" which should have been validated earlier: "foo**?$.desktop" contains a reserved apparmor char`)
}

func (s *desktopFileRulesBaseSuite) TestDesktopFileRulesBadDesktopFileIDs(c *C) {
logbuf, restore := logger.MockLogger()
defer restore()

// Stress the case where a desktop file ids attribute skipped validation during
// installation somehow
restore := builtin.MockDesktopFilesFromMount(func(s *snap.Info) ([]string, error) {
restore = builtin.MockDesktopFilesFromInstalledSnap(func(s *snap.Info) ([]string, error) {
return []string{"org.*.example.desktop"}, nil
})
defer restore()
Expand All @@ -410,4 +429,6 @@ func (s *desktopFileRulesBaseSuite) TestDesktopFileRulesBadDesktopFileIDs(c *C)
expectedRules: s.fallbackRules,
}
s.testDesktopFileRules(c, opts)

c.Check(logbuf.String(), testutil.Contains, `internal error: invalid desktop file id "org.*.example" found in snap "some-snap" which should have failed in BeforePreparePlug checks: "org.*.example" contains a reserved apparmor char`)
}
31 changes: 19 additions & 12 deletions snap/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,21 @@ func (s *Info) DesktopPlugFileIDs() ([]string, error) {
return desktopFileIDs, nil
}

func sanitizeDesktopFileName(base string) string {
var b strings.Builder
b.Grow(len(base))

for _, c := range base {
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.' {
b.WriteRune(c)
} else {
b.WriteRune('_')
}
}

return b.String()
}

// MangleDesktopFileName returns the sanitized file name prefixed with Info.DesktopPrefix().
// If the passed name (without the .desktop extension) is listed under the desktop-file-ids
// desktop interface plug attribute then the desktop file name is returned as is without
Expand Down Expand Up @@ -1024,26 +1039,18 @@ func (s *Info) MangleDesktopFileName(desktopFile string) (string, error) {

// Sanitization logic shouldn't worry about being backware compatible because the
// desktop files are always written when snapd starts in ensureDesktopFilesUpdated.
sanitizedBase := ""
for _, c := range base {
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.' {
sanitizedBase += string(c)
continue
}
// Replace with '_'
sanitizedBase += "_"
}
sanitizedBase := sanitizeDesktopFileName(base)

return filepath.Join(dir, fmt.Sprintf("%s_%s", s.DesktopPrefix(), sanitizedBase)), nil
}

type DesktopFilesFromMountOptions struct {
type DesktopFilesFromInstalledSnapOptions struct {
// Mangles found desktop files using Info.MangleDesktopFileName()
MangleFileNames bool
}

// DesktopFilesFromMount returns the desktop files found under <snap-mount>/meta/gui.
func (s *Info) DesktopFilesFromMount(opts DesktopFilesFromMountOptions) ([]string, error) {
// DesktopFilesFromInstalledSnap returns the desktop files found under <snap-mount>/meta/gui.
func (s *Info) DesktopFilesFromInstalledSnap(opts DesktopFilesFromInstalledSnapOptions) ([]string, error) {
rootDir := filepath.Join(s.MountDir(), "meta", "gui")
if !osutil.IsDirectory(rootDir) {
return nil, nil
Expand Down
18 changes: 9 additions & 9 deletions snap/info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2651,7 +2651,7 @@ plugs:
c.Assert(err, NotNil)
}

func (s *infoSuite) TestDesktopFilesFromMountNoFiles(c *C) {
func (s *infoSuite) TestDesktopFilesFromInstalledSnapNoFiles(c *C) {
const desktopAppYaml = `
name: foo
version: 1.0
Expand All @@ -2660,12 +2660,12 @@ version: 1.0
info, err := snap.InfoFromSnapYaml([]byte(desktopAppYaml))
c.Assert(err, IsNil)

desktopFiles, err := info.DesktopFilesFromMount(snap.DesktopFilesFromMountOptions{})
desktopFiles, err := info.DesktopFilesFromInstalledSnap(snap.DesktopFilesFromInstalledSnapOptions{})
c.Assert(err, IsNil)
c.Assert(desktopFiles, IsNil)
}

func (s *infoSuite) testDesktopFilesFromMount(c *C, mangle bool) {
func (s *infoSuite) testDesktopFilesFromInstalledSnap(c *C, mangle bool) {
const desktopAppYaml = `
name: foo
version: 1.0
Expand All @@ -2687,8 +2687,8 @@ plugs:
c.Assert(err, IsNil)
}

opts := snap.DesktopFilesFromMountOptions{MangleFileNames: mangle}
desktopFilesFound, err := info.DesktopFilesFromMount(opts)
opts := snap.DesktopFilesFromInstalledSnapOptions{MangleFileNames: mangle}
desktopFilesFound, err := info.DesktopFilesFromInstalledSnap(opts)
c.Assert(err, IsNil)
c.Assert(desktopFilesFound, HasLen, len(testDesktopFiles))

Expand All @@ -2711,12 +2711,12 @@ plugs:
}
}

func (s *infoSuite) TestDesktopFilesFromMount(c *C) {
func (s *infoSuite) TestDesktopFilesFromInstalledSnap(c *C) {
const mangle = false
s.testDesktopFilesFromMount(c, mangle)
s.testDesktopFilesFromInstalledSnap(c, mangle)
}

func (s *infoSuite) TestDesktopFilesFromMountMangled(c *C) {
func (s *infoSuite) TestDesktopFilesFromInstalledSnapMangled(c *C) {
const mangle = true
s.testDesktopFilesFromMount(c, mangle)
s.testDesktopFilesFromInstalledSnap(c, mangle)
}
5 changes: 3 additions & 2 deletions wrappers/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func findDesktopFiles(rootDir string) ([]string, error) {
}

func deriveDesktopFilesContent(s *snap.Info) (map[string]osutil.FileState, error) {
desktopFiles, err := s.DesktopFilesFromMount(snap.DesktopFilesFromMountOptions{})
desktopFiles, err := s.DesktopFilesFromInstalledSnap(snap.DesktopFilesFromInstalledSnapOptions{})
if err != nil {
return nil, err
}
Expand All @@ -261,7 +261,8 @@ func deriveDesktopFilesContent(s *snap.Info) (map[string]osutil.FileState, error
return nil, err
}
if _, exists := content[base]; exists {
ZeyadYasser marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("duplicate desktop file name after mangling")
logger.Noticef("skipping %q as a duplicate file name %q was found after mangling", filepath.Base(df), base)
ZeyadYasser marked this conversation as resolved.
Show resolved Hide resolved
continue
}
fileContent, err := os.ReadFile(df)
if err != nil {
Expand Down
Loading
Loading