mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-06 21:48:30 -04:00
core: fix security and concurrency issues found in a backend audit (#2805)
* core: fix security and concurrency issues found in backend audit
Security:
- privesc: pipe the sudo password via stdin (sudo -S) instead of
embedding it in the command string, so it no longer appears in argv
(readable by any local user via /proc/<pid>/cmdline or ps)
- greeter: tokenize a session .desktop Exec= line into argv and execve
directly instead of running it through /bin/sh -c, closing a command-
injection path via user-writable ~/.local/share/wayland-sessions
- plugins: reject path-separator/.. in plugin id/name before joining
into a filesystem path, closing an arbitrary-directory-delete in the
uninstall/update fallback
- keybinds/hyprland: always quote unrecognized bind actions/keys when
writing generated Lua; only re-emit genuine round-tripped custom Lua
verbatim (tracked via an explicit flag), closing a Lua-injection path
- desktop/mimeapps: reject newline/bracket in mime/desktop-id fields so
they can't inject fake sections into the shared mimeapps.list
Robustness / concurrency:
- server: recover panics in the request-dispatch path so one bad
handler can't crash the daemon and drop every client
- go-wayland: recover panics in the shared dispatch choke point so a
malformed compositor event can't crash CLI tools / the daemon
- server: per-connection D-Bus client ID instead of a shared constant,
fixing cross-client signal delivery and subscription teardown
- network: guard the NetworkManager device maps with a mutex (a
concurrent map read/write here is an unrecoverable fatal error)
- cups: close the event channel on Stop() so Unsubscribe() of the last
subscriber no longer deadlocks; allocate the fresh channel in Start()
- freedesktop: reuse the shared session conn for the settings watcher
and tear it down in Close(), fixing a per-Manager conn+goroutine leak
- clipboard: mutex-guard lazy dbusConn creation
- geolocation: use WithMatchMember for the GeoClue2 LocationUpdated
signal (was WithMatchSender with an interface.member string, so the
match never fired and live location updates never arrived)
- screenshot: set failed=true on buffer/pool creation errors so the
dispatch loop doesn't wait forever for a ready/failed that never comes
* apply code review comments
---------
Co-authored-by: bbedward <bbedward@gmail.com>
(cherry picked from commit ca89e12963)
This commit is contained in:
@@ -529,11 +529,23 @@ func execFromDesktopFile(path string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
inDesktopEntry := false
|
||||
for line := range strings.SplitSeq(string(data), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "Exec=") {
|
||||
return strings.TrimSpace(trimmed[len("Exec="):]), nil
|
||||
switch {
|
||||
case trimmed == "" || strings.HasPrefix(trimmed, "#"):
|
||||
continue
|
||||
case strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]"):
|
||||
inDesktopEntry = trimmed == "[Desktop Entry]"
|
||||
continue
|
||||
case !inDesktopEntry:
|
||||
continue
|
||||
}
|
||||
key, value, found := strings.Cut(trimmed, "=")
|
||||
if !found || strings.TrimSpace(key) != "Exec" {
|
||||
continue
|
||||
}
|
||||
return strings.TrimSpace(value), nil
|
||||
}
|
||||
return "", fmt.Errorf("no Exec= line found in %s", path)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package greeter
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -93,18 +94,95 @@ func resolveSessionExecInDirs(sessionID string, dirs []string) (string, error) {
|
||||
return "", fmt.Errorf("session desktop file %q was not found", id)
|
||||
}
|
||||
|
||||
// parseExecString splits a Desktop Entry Exec= value into argv without
|
||||
// involving a shell, mirroring quickshell's DesktopEntry::parseExecString
|
||||
// (string quoting, value escapes, field code stripping).
|
||||
func parseExecString(execLine string) []string {
|
||||
var args []string
|
||||
var cur strings.Builder
|
||||
inString := false
|
||||
escape := 0
|
||||
percent := false
|
||||
|
||||
for _, c := range execLine {
|
||||
switch {
|
||||
case escape == 0 && c == '\\':
|
||||
escape = 1
|
||||
case inString:
|
||||
switch {
|
||||
case c == '\\':
|
||||
escape++
|
||||
if escape == 4 {
|
||||
cur.WriteByte('\\')
|
||||
escape = 0
|
||||
}
|
||||
case escape == 2:
|
||||
cur.WriteRune(c)
|
||||
escape = 0
|
||||
case escape != 0:
|
||||
switch c {
|
||||
case 's':
|
||||
cur.WriteByte(' ')
|
||||
case 'n':
|
||||
cur.WriteByte('\n')
|
||||
case 't':
|
||||
cur.WriteByte('\t')
|
||||
case 'r':
|
||||
cur.WriteByte('\r')
|
||||
default:
|
||||
cur.WriteRune(c)
|
||||
}
|
||||
escape = 0
|
||||
case c == '"' || c == '\'':
|
||||
inString = false
|
||||
default:
|
||||
cur.WriteRune(c)
|
||||
}
|
||||
case escape != 0:
|
||||
cur.WriteRune(c)
|
||||
escape = 0
|
||||
case percent:
|
||||
if c == '%' {
|
||||
cur.WriteByte('%')
|
||||
}
|
||||
percent = false
|
||||
case c == '%':
|
||||
percent = true
|
||||
case c == '"' || c == '\'':
|
||||
inString = true
|
||||
case c == ' ':
|
||||
if cur.Len() > 0 {
|
||||
args = append(args, cur.String())
|
||||
cur.Reset()
|
||||
}
|
||||
default:
|
||||
cur.WriteRune(c)
|
||||
}
|
||||
}
|
||||
if cur.Len() > 0 {
|
||||
args = append(args, cur.String())
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func LaunchSessionByID(sessionID string) error {
|
||||
execLine, err := ResolveSessionExec(sessionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
execLine = strings.TrimSpace(stripDesktopExecCodes(execLine))
|
||||
if execLine == "" {
|
||||
|
||||
argv := parseExecString(strings.TrimSpace(execLine))
|
||||
if len(argv) == 0 {
|
||||
return fmt.Errorf("session %q has an empty Exec command", sessionID)
|
||||
}
|
||||
|
||||
resolved, err := exec.LookPath(argv[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("session %q command %q not found: %w", sessionID, argv[0], err)
|
||||
}
|
||||
|
||||
env := append(os.Environ(), "XDG_SESSION_TYPE=wayland")
|
||||
return syscall.Exec("/bin/sh", []string{"sh", "-c", "exec " + execLine}, env)
|
||||
return syscall.Exec(resolved, argv, env)
|
||||
}
|
||||
|
||||
func LaunchSessionFromMemory(cacheDir, homeDir string) error {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package greeter
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseExecString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
exec string
|
||||
want []string
|
||||
}{
|
||||
{"plain", "niri --session", []string{"niri", "--session"}},
|
||||
{"extra spaces", "niri --session", []string{"niri", "--session"}},
|
||||
{"double quoted arg", `env "with space" run`, []string{"env", "with space", "run"}},
|
||||
{"single quoted arg", `env 'with space' run`, []string{"env", "with space", "run"}},
|
||||
{"escaped quote in quotes", `sh "say \\"hi\\""`, []string{"sh", `say "hi"`}},
|
||||
{"field code dropped", "gnome-session %U", []string{"gnome-session"}},
|
||||
{"field code mid-arg", "app --url=%u --run", []string{"app", "--url=", "--run"}},
|
||||
{"literal percent", "app 100%% done", []string{"app", "100%", "done"}},
|
||||
{"shell metachars stay literal", "sh -c $(reboot); echo", []string{"sh", "-c", "$(reboot);", "echo"}},
|
||||
{"empty", "", nil},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := parseExecString(tt.exec); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("parseExecString(%q) = %#v, want %#v", tt.exec, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecFromDesktopFileOnlyReadsDesktopEntryGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "example.desktop")
|
||||
writeTestFile(t, path, `[Desktop Action other]
|
||||
Exec=/wrong/binary
|
||||
|
||||
[Desktop Entry]
|
||||
Name=Example
|
||||
Exec = /right/binary --flag
|
||||
`)
|
||||
|
||||
got, err := execFromDesktopFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("execFromDesktopFile returned error: %v", err)
|
||||
}
|
||||
if got != "/right/binary --flag" {
|
||||
t.Fatalf("execFromDesktopFile = %q, want %q", got, "/right/binary --flag")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user