Harden session launch command recovery

This commit is contained in:
cheerfulScumbag 2026-03-23 19:09:13 +00:00
parent 818f80c068
commit e2b63a06d7
3 changed files with 380 additions and 83 deletions

View file

@ -82,10 +82,14 @@ idleinhibit_ignore_visible=0
# Disabled by default to preserve Mango's current behavior.
# Minimized restore is currently unsupported.
# Restore expects target outputs to already exist before clients map.
# Mango only restores from a trusted session file owned by the current user
# and not writable by group or others.
session_restore=0
# Relaunch mapping used when Mango cannot infer a launch command from a client.
# Optional relaunch override used when Mango cannot recover a suitable launcher.
# Format: session_launch=app_id|command
# or: session_launch=app_id|title|command
# Mango prefers exact Mango-owned spawn commands, then normalized desktop/Flatpak
# launchers, then raw process argv before consulting session_launch.
# session_launch=foot|foot
# session_launch=foot|gamma|foot -a foot -T gamma -e sh -lc "sleep 600"
# session_launch=org.kde.dolphin|dolphin .

View file

@ -775,6 +775,7 @@ void mango_session_remember_client_launch_command(Client *c,
static char *mango_session_recover_process_command(pid_t pid);
static void mango_session_attach_process_command(Client *c);
static char *mango_session_find_desktop_exec(const char *app_id);
static char *mango_session_find_flatpak_command(const char *app_id);
static char *mango_session_normalize_launch_command(Client *c, pid_t pid);
static void scene_buffer_apply_effect(struct wlr_scene_buffer *buffer,
int32_t sx, int32_t sy, void *data);
@ -1207,42 +1208,112 @@ static void mango_session_trim_newline(char *s) {
}
static char *mango_session_strip_exec_field_codes(const char *exec) {
char *copy, *token, *saveptr = NULL, *result;
char **tokens = NULL;
char *current = NULL;
char *result = NULL;
size_t token_count = 0, token_capacity = 0;
size_t current_len = 0, current_capacity = 0;
bool in_single_quotes = false;
bool in_double_quotes = false;
const char *p;
if (!exec || exec[0] == '\0')
return NULL;
copy = strdup(exec);
if (!copy)
return NULL;
for (p = exec; *p != '\0'; ++p) {
char ch = *p;
result = strdup("");
for (token = strtok_r(copy, " \t", &saveptr); token != NULL;
token = strtok_r(NULL, " \t", &saveptr)) {
char *next;
if (!in_single_quotes && !in_double_quotes &&
(ch == ' ' || ch == '\t')) {
if (current_len > 0) {
char **next_tokens;
if (token[0] == '%' && token[1] != '\0')
if (token_count + 1 > token_capacity) {
size_t next_capacity =
token_capacity == 0 ? 8 : token_capacity * 2;
next_tokens =
realloc(tokens, next_capacity * sizeof(*tokens));
if (!next_tokens)
goto cleanup;
tokens = next_tokens;
token_capacity = next_capacity;
}
current[current_len] = '\0';
tokens[token_count++] = current;
current = NULL;
current_len = 0;
current_capacity = 0;
}
continue;
if (strcmp(token, "%%") == 0)
token = "%";
next = result[0] == '\0' ? string_printf("%s", token)
: string_printf("%s %s", result, token);
free(result);
result = next;
if (!result) {
free(copy);
return NULL;
}
if (!in_double_quotes && ch == '\'') {
in_single_quotes = !in_single_quotes;
continue;
}
if (!in_single_quotes && ch == '"') {
in_double_quotes = !in_double_quotes;
continue;
}
if (ch == '\\' && p[1] != '\0') {
ch = *++p;
} else if (ch == '%' && p[1] != '\0') {
if (p[1] == '%') {
ch = '%';
++p;
} else {
++p;
continue;
}
}
if (current_len + 2 > current_capacity) {
size_t next_capacity = current_capacity == 0 ? 32 : current_capacity * 2;
char *next_current = realloc(current, next_capacity);
if (!next_current)
goto cleanup;
current = next_current;
current_capacity = next_capacity;
}
current[current_len++] = ch;
}
free(copy);
if (current_len > 0) {
char **next_tokens;
if (result[0] == '\0') {
if (token_count + 1 > token_capacity) {
size_t next_capacity = token_capacity == 0 ? 8 : token_capacity * 2;
next_tokens = realloc(tokens, next_capacity * sizeof(*tokens));
if (!next_tokens)
goto cleanup;
tokens = next_tokens;
}
current[current_len] = '\0';
tokens[token_count++] = current;
current = NULL;
}
result = mango_session_join_argv(tokens, token_count);
cleanup:
free(current);
if (tokens) {
for (size_t i = 0; i < token_count; ++i)
free(tokens[i]);
free(tokens);
}
if (!result || result[0] == '\0') {
free(result);
return NULL;
}
return result;
}
@ -1251,43 +1322,211 @@ static bool mango_session_path_exists(const char *path) {
return path && stat(path, &st) == 0;
}
static bool mango_session_desktop_matches(const char *desktop_path,
const char *app_id,
char **exec_out) {
typedef struct {
char *path;
char *exec_value;
char *startup_wm_class;
char *flatpak_id;
} MangoSessionDesktopEntry;
static void mango_session_free_desktop_entry(MangoSessionDesktopEntry *entry) {
if (!entry)
return;
free(entry->path);
free(entry->exec_value);
free(entry->startup_wm_class);
free(entry->flatpak_id);
memset(entry, 0, sizeof(*entry));
}
static bool mango_session_append_search_dir(char ***dirs, size_t *count,
size_t *capacity,
const char *dir) {
char **next_dirs;
if (!dirs || !count || !capacity || !dir || dir[0] == '\0')
return true;
if (*count > 0 && strcmp((*dirs)[*count - 1], dir) == 0)
return true;
if (*count + 1 >= *capacity) {
size_t next_capacity = *capacity == 0 ? 8 : *capacity * 2;
next_dirs = realloc(*dirs, next_capacity * sizeof(**dirs));
if (!next_dirs)
return false;
*dirs = next_dirs;
*capacity = next_capacity;
}
(*dirs)[*count] = strdup(dir);
if (!(*dirs)[*count])
return false;
(*count)++;
(*dirs)[*count] = NULL;
return true;
}
static bool mango_session_append_data_home_applications(char ***dirs, size_t *count,
size_t *capacity) {
const char *xdg_data_home = getenv("XDG_DATA_HOME");
const char *home = getenv("HOME");
char *applications_dir = NULL;
bool ok;
if (xdg_data_home && xdg_data_home[0] != '\0')
applications_dir = string_printf("%s/applications", xdg_data_home);
else if (home && home[0] != '\0')
applications_dir = string_printf("%s/.local/share/applications", home);
if (!applications_dir)
return true;
ok = mango_session_append_search_dir(dirs, count, capacity, applications_dir);
free(applications_dir);
return ok;
}
static bool mango_session_append_data_dirs_applications(char ***dirs, size_t *count,
size_t *capacity) {
const char *xdg_data_dirs = getenv("XDG_DATA_DIRS");
const char *cursor;
if (!xdg_data_dirs || xdg_data_dirs[0] == '\0')
xdg_data_dirs = "/usr/local/share:/usr/share";
for (cursor = xdg_data_dirs; cursor && cursor[0] != '\0';) {
const char *separator = strchr(cursor, ':');
size_t len = separator ? (size_t)(separator - cursor) : strlen(cursor);
char *base_dir;
char *applications_dir;
bool ok;
if (len == 0) {
cursor = separator ? separator + 1 : NULL;
continue;
}
base_dir = strndup(cursor, len);
if (!base_dir)
return false;
applications_dir = string_printf("%s/applications", base_dir);
free(base_dir);
if (!applications_dir)
return false;
ok = mango_session_append_search_dir(dirs, count, capacity,
applications_dir);
free(applications_dir);
if (!ok)
return false;
cursor = separator ? separator + 1 : NULL;
}
return true;
}
static char **mango_session_desktop_search_dirs(void) {
char **dirs = NULL;
size_t count = 0, capacity = 0;
const char *home = getenv("HOME");
char *user_flatpak_dir = NULL;
if (!mango_session_append_data_home_applications(&dirs, &count, &capacity) ||
!mango_session_append_data_dirs_applications(&dirs, &count, &capacity))
goto fail;
if (!mango_session_append_search_dir(&dirs, &count, &capacity,
"/var/lib/flatpak/exports/share/applications"))
goto fail;
if (home && home[0] != '\0') {
user_flatpak_dir = string_printf(
"%s/.local/share/flatpak/exports/share/applications", home);
if (!user_flatpak_dir)
goto fail;
if (!mango_session_append_search_dir(&dirs, &count, &capacity,
user_flatpak_dir))
goto fail;
}
free(user_flatpak_dir);
return dirs;
fail:
free(user_flatpak_dir);
if (dirs) {
for (size_t i = 0; i < count; ++i)
free(dirs[i]);
free(dirs);
}
return NULL;
}
static void mango_session_free_search_dirs(char **dirs) {
size_t i;
if (!dirs)
return;
for (i = 0; dirs[i] != NULL; ++i)
free(dirs[i]);
free(dirs);
}
static bool mango_session_load_desktop_entry(const char *desktop_path,
MangoSessionDesktopEntry *entry) {
FILE *f;
char line[2048];
char *exec_value = NULL;
char *startup_wm_class = NULL;
char *flatpak_id = NULL;
char *desktop_id = NULL;
bool matched = false;
const char *basename;
char *dot;
if (!desktop_path || !app_id || app_id[0] == '\0')
if (!desktop_path || !entry)
return false;
memset(entry, 0, sizeof(*entry));
entry->path = strdup(desktop_path);
if (!entry->path)
return false;
f = fopen(desktop_path, "r");
if (!f)
if (!f) {
mango_session_free_desktop_entry(entry);
return false;
}
while (fgets(line, sizeof(line), f)) {
mango_session_trim_newline(line);
if (strncmp(line, "Exec=", 5) == 0) {
free(exec_value);
exec_value = strdup(line + 5);
free(entry->exec_value);
entry->exec_value = strdup(line + 5);
} else if (strncmp(line, "StartupWMClass=", 15) == 0) {
free(startup_wm_class);
startup_wm_class = strdup(line + 15);
free(entry->startup_wm_class);
entry->startup_wm_class = strdup(line + 15);
} else if (strncmp(line, "X-Flatpak=", 10) == 0) {
free(flatpak_id);
flatpak_id = strdup(line + 10);
free(entry->flatpak_id);
entry->flatpak_id = strdup(line + 10);
}
}
fclose(f);
basename = strrchr(desktop_path, '/');
basename = basename ? basename + 1 : desktop_path;
return true;
}
static bool mango_session_desktop_entry_matches(
const MangoSessionDesktopEntry *entry, const char *app_id) {
const char *basename;
char *desktop_id;
char *dot;
bool matched = false;
if (!entry || !entry->path || !app_id || app_id[0] == '\0')
return false;
basename = strrchr(entry->path, '/');
basename = basename ? basename + 1 : entry->path;
desktop_id = strdup(basename);
if (desktop_id) {
dot = strrchr(desktop_id, '.');
@ -1298,30 +1537,28 @@ static bool mango_session_desktop_matches(const char *desktop_path,
free(desktop_id);
}
if (!matched && startup_wm_class && strcmp(startup_wm_class, app_id) == 0)
if (!matched && entry->startup_wm_class &&
strcmp(entry->startup_wm_class, app_id) == 0)
matched = true;
if (!matched && flatpak_id && strcmp(flatpak_id, app_id) == 0)
if (!matched && entry->flatpak_id && strcmp(entry->flatpak_id, app_id) == 0)
matched = true;
if (matched && exec_value && exec_out)
*exec_out = mango_session_strip_exec_field_codes(exec_value);
free(exec_value);
free(startup_wm_class);
free(flatpak_id);
return matched;
}
static char *mango_session_find_desktop_exec(const char *app_id) {
const char *dirs[] = {
"/home/kenn/.local/share/applications",
"/etc/profiles/per-user/kenn/share/applications",
"/run/current-system/sw/share/applications",
"/var/lib/flatpak/exports/share/applications",
NULL,
};
static bool mango_session_find_desktop_entry(const char *app_id,
MangoSessionDesktopEntry *result) {
char **dirs = mango_session_desktop_search_dirs();
bool found = false;
for (size_t i = 0; dirs[i] != NULL; ++i) {
if (!result)
return false;
memset(result, 0, sizeof(*result));
if (!dirs)
return false;
for (size_t i = 0; dirs[i] != NULL && !found; ++i) {
DIR *dir = opendir(dirs[i]);
struct dirent *entry;
@ -1329,27 +1566,66 @@ static char *mango_session_find_desktop_exec(const char *app_id) {
continue;
while ((entry = readdir(dir)) != NULL) {
MangoSessionDesktopEntry candidate;
char *path;
char *exec_value = NULL;
bool matched;
if (!strstr(entry->d_name, ".desktop"))
continue;
path = string_printf("%s/%s", dirs[i], entry->d_name);
matched = mango_session_desktop_matches(path, app_id, &exec_value);
free(path);
if (!matched)
if (!path)
continue;
closedir(dir);
return exec_value;
if (!mango_session_load_desktop_entry(path, &candidate)) {
free(path);
continue;
}
free(path);
if (!mango_session_desktop_entry_matches(&candidate, app_id)) {
mango_session_free_desktop_entry(&candidate);
continue;
}
*result = candidate;
found = true;
break;
}
closedir(dir);
}
return NULL;
mango_session_free_search_dirs(dirs);
return found;
}
static char *mango_session_find_desktop_exec(const char *app_id) {
MangoSessionDesktopEntry entry;
char *exec_value;
if (!mango_session_find_desktop_entry(app_id, &entry))
return NULL;
exec_value = entry.exec_value
? mango_session_strip_exec_field_codes(entry.exec_value)
: NULL;
mango_session_free_desktop_entry(&entry);
return exec_value;
}
static char *mango_session_find_flatpak_command(const char *app_id) {
MangoSessionDesktopEntry entry;
char *command = NULL;
if (!mango_session_find_desktop_entry(app_id, &entry))
return NULL;
if (entry.flatpak_id && strcmp(entry.flatpak_id, app_id) == 0)
command = entry.exec_value ? mango_session_strip_exec_field_codes(entry.exec_value)
: NULL;
mango_session_free_desktop_entry(&entry);
return command;
}
static char *mango_session_read_cmdline(pid_t pid, size_t *argc_out) {
@ -1441,26 +1717,16 @@ static char *mango_session_recover_process_command(pid_t pid) {
static char *mango_session_normalize_launch_command(Client *c, pid_t pid) {
const char *app_id;
char *flatpak_command;
char *desktop_exec;
char *raw_command;
app_id = c ? client_get_appid(c) : NULL;
if (app_id && app_id[0] != '\0' && strcmp(app_id, "broken") != 0) {
char *flatpak_desktop = string_printf(
"/var/lib/flatpak/exports/share/applications/%s.desktop", app_id);
char *user_flatpak_desktop = string_printf(
"%s/.local/share/flatpak/exports/share/applications/%s.desktop",
getenv("HOME") ? getenv("HOME") : "", app_id);
if ((flatpak_desktop && mango_session_path_exists(flatpak_desktop)) ||
(user_flatpak_desktop &&
mango_session_path_exists(user_flatpak_desktop))) {
free(flatpak_desktop);
free(user_flatpak_desktop);
return string_printf("flatpak run %s", app_id);
}
free(flatpak_desktop);
free(user_flatpak_desktop);
flatpak_command = mango_session_find_flatpak_command(app_id);
if (flatpak_command && flatpak_command[0] != '\0')
return flatpak_command;
free(flatpak_command);
desktop_exec = mango_session_find_desktop_exec(app_id);
if (desktop_exec && desktop_exec[0] != '\0')

View file

@ -82,6 +82,23 @@ static char *session_file_path(void) {
return path;
}
static bool session_file_is_trusted(const char *path) {
struct stat st;
uid_t uid = getuid();
if (!path || stat(path, &st) != 0)
return false;
if (!S_ISREG(st.st_mode))
return false;
if (st.st_uid != uid)
return false;
if ((st.st_mode & (S_IWGRP | S_IWOTH)) != 0)
return false;
return true;
}
static void free_pending_entries(void) {
free(pending_entries);
pending_entries = NULL;
@ -275,6 +292,16 @@ static bool load_pending_entries(void) {
if (!path)
return false;
if (access(path, F_OK) != 0)
goto cleanup;
if (!session_file_is_trusted(path)) {
fprintf(stderr,
"mango session: refusing to restore from untrusted session file: %s\n",
path);
goto cleanup;
}
in = fopen(path, "r");
if (!in)
goto cleanup;