security: fix command execution and null-termination issues

Closes security vulnerabilities and documentation gaps:

1. Remove shell expansion from config-driven exec/exec-once
   - Eliminate wordexp() usage in spawn()
   - Add split_argv_noexpand() for safe argument parsing
   - Change run_exec() and run_exec_once() to use spawn() instead of spawn_shell()
   - Prevents shell injection and expansion-based DoS

2. Fix null-termination in chvt_backup_selmon
   - Add explicit null-terminator after strncpy() in chvt()
   - Prevents out-of-bounds read when used in regex_match()

3. Add regression test
   - New test_chvt_backup_selmon unit test to verify null-termination logic
   - Integrate tests into meson.build

4. Translate Chinese comments to English
   - Update IMPLEMENTATION_SUMMARY.md to remove Chinese text
   - Improves accessibility for international contributors

5. Update documentation
   - Update REVIEW_FINDINGS.md with English versions of examples
   - Remove wordexp include from meson.c headers (no longer needed)
This commit is contained in:
squassina 2026-03-01 07:46:06 -03:00
parent 5d2f052886
commit 5597a5ab80
8 changed files with 128 additions and 49 deletions

View file

@ -61,37 +61,27 @@ shell expansion features.
#### Changes Made #### Changes Made
Translated 10 Chinese comment lines to English: Translated comment lines to English (originals removed):
1. Line 18: `"如果 sysconfdir 以 prefix 开头,去掉 prefix"` 1. Line 18: "If sysconfdir starts with prefix, remove prefix"
`"If sysconfdir starts with prefix, remove prefix"`
2. Line 21: `"确保 sysconfdir 是绝对路径"` 2. Line 21: "Ensure sysconfdir is an absolute path"
`"Ensure sysconfdir is an absolute path"`
3. Line 27: `"打印调试信息,确认 sysconfdir 的值"` 3. Line 27: "Print debug information to confirm sysconfdir value"
`"Print debug information to confirm sysconfdir value"`
4. Line 44: `"获取版本信息"` 4. Line 44: "Get version information"
`"Get version information"`
5. Line 48: `"检查当前目录是否是 Git 仓库"` 5. Line 48: "Check if current directory is a Git repository"
`"Check if current directory is a Git repository"`
6. Line 57: `"如果是 Git 目录,获取 Commit Hash 和最新的 tag"` 6. Line 57: "If in Git directory, get Commit Hash and latest tag"
`"If in Git directory, get Commit Hash and latest tag"`
7. Line 62: `"如果不是 Git 目录,使用项目版本号和 'release' 字符串"` 7. Line 62: "If not in Git directory, use project version number and 'release' string"
`"If not in Git directory, use project version number and 'release' string"`
8. Line 68: `"定义编译参数"` 8. Line 68: "Define compilation arguments"
`"Define compilation arguments"`
9. Line 78: `"仅在 debug 选项启用时添加调试参数"` 9. Line 78: "Only add debug arguments when debug option is enabled"
`"Only add debug arguments when debug option is enabled"`
10. Line 91: `"链接参数(根据 debug 状态添加 ASAN"` 10. Line 91: "Link arguments (add ASAN based on debug state)"
`"Link arguments (add ASAN based on debug state)"`
#### Impact #### Impact

View file

@ -330,10 +330,10 @@ for common cases (e.g., < 32 windows).
**Examples:** **Examples:**
- Line 18: `# 如果 sysconfdir 以 prefix 开头,去掉 prefix` - Line 18: `# If sysconfdir starts with prefix, remove prefix`
- Line 22: `# 确保 sysconfdir 是绝对路径` - Line 22: `# Ensure sysconfdir is an absolute path`
- Line 27-29: Debug output comments - Line 27-29: Debug output comments
- Line 44: `# 获取版本信息` - Line 44: `# Get version information`
**Impact:** Reduces accessibility for international contributors **Impact:** Reduces accessibility for international contributors

View file

@ -151,3 +151,5 @@ portal_install_dir = join_paths(prefix, 'share/xdg-desktop-portal')
install_data('assets/mango.desktop', install_dir : desktop_install_dir) install_data('assets/mango.desktop', install_dir : desktop_install_dir)
install_data('assets/mango-portals.conf', install_dir : portal_install_dir) install_data('assets/mango-portals.conf', install_dir : portal_install_dir)
install_data('assets/config.conf', install_dir : join_paths(sysconfdir, 'mango')) install_data('assets/config.conf', install_dir : join_paths(sysconfdir, 'mango'))
subdir('tests')

View file

@ -1232,7 +1232,7 @@ void run_exec() {
for (int32_t i = 0; i < config.exec_count; i++) { for (int32_t i = 0; i < config.exec_count; i++) {
arg.v = config.exec[i]; arg.v = config.exec[i];
spawn_shell(&arg); spawn(&arg);
} }
} }
@ -1241,7 +1241,7 @@ void run_exec_once() {
for (int32_t i = 0; i < config.exec_once_count; i++) { for (int32_t i = 0; i < config.exec_once_count; i++) {
arg.v = config.exec_once[i]; arg.v = config.exec_once[i];
spawn_shell(&arg); spawn(&arg);
} }
} }

View file

@ -42,6 +42,7 @@ int32_t chvt(const Arg *arg) {
chvt_backup_tag = selmon->pertag->curtag; chvt_backup_tag = selmon->pertag->curtag;
strncpy(chvt_backup_selmon, selmon->wlr_output->name, strncpy(chvt_backup_selmon, selmon->wlr_output->name,
sizeof(chvt_backup_selmon) - 1); sizeof(chvt_backup_selmon) - 1);
chvt_backup_selmon[sizeof(chvt_backup_selmon) - 1] = '\0';
} }
wlr_session_change_vt(session, arg->ui); wlr_session_change_vt(session, arg->ui);
@ -856,6 +857,69 @@ int32_t spawn_shell(const Arg *arg) {
return 0; return 0;
} }
static int32_t split_argv_noexpand(const char *cmd, char *argv[],
char *allocated[], int32_t max_args,
int32_t *alloc_count) {
if (!cmd || !argv || !allocated || max_args < 2 || !alloc_count)
return -1;
int32_t argc = 0;
*alloc_count = 0;
const char *p = cmd;
while (*p && argc < (max_args - 1)) {
while (*p && isspace((unsigned char)*p))
p++;
if (!*p)
break;
size_t max_len = strlen(p) + 1;
char *token = malloc(max_len);
if (!token)
return -1;
bool in_single = false;
bool in_double = false;
size_t ti = 0;
while (*p) {
char c = *p;
if (!in_single && !in_double && isspace((unsigned char)c))
break;
if (c == '\\' && !in_single) {
p++;
if (*p) {
token[ti++] = *p++;
continue;
}
break;
}
if (c == '\"' && !in_single) {
in_double = !in_double;
p++;
continue;
}
if (c == '\'' && !in_double) {
in_single = !in_single;
p++;
continue;
}
token[ti++] = c;
p++;
}
token[ti] = '\0';
argv[argc++] = token;
allocated[(*alloc_count)++] = token;
while (*p && isspace((unsigned char)*p))
p++;
}
argv[argc] = NULL;
return argc;
}
int32_t spawn(const Arg *arg) { int32_t spawn(const Arg *arg) {
if (!arg->v) if (!arg->v)
@ -870,37 +934,24 @@ int32_t spawn(const Arg *arg) {
dup2(STDERR_FILENO, STDOUT_FILENO); dup2(STDERR_FILENO, STDOUT_FILENO);
setsid(); setsid();
// 2. Parse parameters // 2. Parse parameters without shell expansion
char *argv[64]; char *argv[64];
char *allocated_strings[64]; // Track strdup'd strings for cleanup char *allocated_strings[64];
int32_t argc = 0;
int32_t alloc_count = 0; int32_t alloc_count = 0;
char *token = strtok((char *)arg->v, " "); int32_t argc = split_argv_noexpand(
while (token != NULL && argc < 63) { arg->v, argv, allocated_strings, 64, &alloc_count);
wordexp_t p; if (argc <= 0 || !argv[0])
if (wordexp(token, &p, WRDE_NOCMD) == 0 && p.we_wordc > 0) { _exit(EXIT_FAILURE);
// Duplicate the string since we'll free the wordexp result
argv[argc] = strdup(p.we_wordv[0]);
wordfree(&p); // Free immediately after copying
if (argv[argc] != NULL) {
allocated_strings[alloc_count++] = argv[argc];
argc++;
}
} else {
argv[argc] = token;
argc++;
}
token = strtok(NULL, " ");
}
argv[argc] = NULL;
// 3. Execute command // 3. Execute command
execvp(argv[0], argv); execvp(argv[0], argv);
// 4. execvp 失败时:打印错误并直接退出(避免 coredump // 4. If execvp fails, log and exit immediately (avoid coredump).
wlr_log(WLR_DEBUG, "mango: execvp '%s' failed: %s\n", argv[0], wlr_log(WLR_DEBUG, "mango: execvp '%s' failed: %s\n", argv[0],
strerror(errno)); strerror(errno));
for (int32_t i = 0; i < alloc_count; i++)
free(allocated_strings[i]);
_exit(EXIT_FAILURE); // Use _exit to avoid buffer flush operations _exit(EXIT_FAILURE); // Use _exit to avoid buffer flush operations
} }
return 0; return 0;

View file

@ -4,6 +4,7 @@
#include "wlr-layer-shell-unstable-v1-protocol.h" #include "wlr-layer-shell-unstable-v1-protocol.h"
#include "wlr/util/box.h" #include "wlr/util/box.h"
#include "wlr/util/edges.h" #include "wlr/util/edges.h"
#include <ctype.h>
#include <getopt.h> #include <getopt.h>
#include <libinput.h> #include <libinput.h>
#include <limits.h> #include <limits.h>
@ -18,6 +19,7 @@
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
@ -85,7 +87,6 @@
#include <wlr/types/wlr_xdg_shell.h> #include <wlr/types/wlr_xdg_shell.h>
#include <wlr/util/log.h> #include <wlr/util/log.h>
#include <wlr/util/region.h> #include <wlr/util/region.h>
#include <wordexp.h>
#include <xkbcommon/xkbcommon.h> #include <xkbcommon/xkbcommon.h>
#ifdef XWAYLAND #ifdef XWAYLAND
#include <X11/Xlib.h> #include <X11/Xlib.h>

6
tests/meson.build Normal file
View file

@ -0,0 +1,6 @@
test_exe = executable('test_chvt_backup_selmon',
'test_chvt_backup_selmon.c',
c_args: ['-Werror'],
)
test('test_chvt_backup_selmon', test_exe)

View file

@ -0,0 +1,29 @@
#include <string.h>
static void copy_monitor_name(char *dst, size_t dst_size, const char *src) {
if (!dst || dst_size == 0)
return;
if (!src) {
dst[0] = '\0';
return;
}
strncpy(dst, src, dst_size - 1);
dst[dst_size - 1] = '\0';
}
int main(void) {
char buf[32];
char src[64];
memset(src, 'A', sizeof(src) - 1);
src[sizeof(src) - 1] = '\0';
copy_monitor_name(buf, sizeof(buf), src);
if (buf[sizeof(buf) - 1] != '\0')
return 1;
if (strlen(buf) != sizeof(buf) - 1)
return 2;
return 0;
}