# Peony 右键"打开终端"时好时坏 Bug：根因分析与修复记录

> 环境：Kylin-Desktop-V10-SP1-HWE-2403（x86_64）· UKUI · peony 3.26.0.0-0k6.17 ·
> peony-open-terminal 插件 3.26.0.0-0k1.22 · mate-terminal 1.24.0 · Qt 5.12.12-kylin
> 结论：**插件 `mate-terminal-menu-plugin.cpp` 中一处 C++ 临时对象生命周期错误（悬空指针，
> use-after-free）导致 `g_spawn_async()` 拿到被复用的堆内存当作工作目录，chdir 失败后静默退出。
> 已修复、编译并部署到本机，真机验证通过。**

---

## 1. 问题现象

- 在 peony 文件管理器中，于文件夹内右键选择"打开终端"（插件 peony-open-terminal 提供的菜单项），
  **有时终端完全不弹出、无任何报错**；
- 表现为"按路径决定"：例如在 `/home/new211/peony/peony-3.2.3` 下必现失败，在 `/tmp`、
  `/home/new211` 等路径下大多能成功，且同一目录的行为在同一系统上是稳定的；
- 该 bug 在多个 Kylin-V10 版本（peony 3.2.x ~ 3.26）上长期存在；
- 对照组：PCManFM 1.3.1"在终端中打开当前文件夹"以及 Nautilus 脚本方式 **100% 成功**，
  说明 mate-terminal 本身和桌面环境没有问题，问题只在 peony 插件。

## 2. 排查过程

1. **定位功能实现**：右键"打开终端"由 `peony-extensions` 的
   `peony-menu-plugin-mate-terminal` 插件实现（系统内为
   `/usr/lib/x86_64-linux-gnu/peony-extensions/libpeony-menu-plugin-mate-terminal.so`，
   源码对应工作区 `peony-extensions-3.2.3/peony-menu-plugin-mate-terminal/mate-terminal-menu-plugin.cpp`）。
2. **反汇编已安装的 3.26 二进制**，确认其 `openTerminal()` 与 3.2.3 源码同构：
   `QUrl::path()` → `QString::toUtf8_helper()` → `g_shell_parse_argv` → `g_spawn_async`，
   且 3.26 版本在 spawn 失败后**只写日志、没有任何兜底重试**（旧版源码中的 `tryOpenAgain()`
   已不再被调用）。
3. **真实桌面复现**：在本机图形会话用 xdotool 模拟"打开 peony 窗口 → 右键 → 点击『打开终端』"
   （菜单行坐标通过对菜单截图做图像识别 + X 窗口几何差集获得）。在
   `/home/new211/peony/peony-3.2.3` 连续 3 次点击，**0 次弹出终端、0 个 mate-terminal 进程**，
   与用户报告一致。
4. **无头差分测试（A/B harness）**：编写 Qt 测试程序直接 `QPluginLoader` 加载插件 .so，
   反复调用 `menuActions()->trigger()`，并用一个"假 mate-terminal"脚本（放在 PATH 前部）
   记录每次被 spawn 时的工作目录。新旧插件对比数据见 §5。

## 3. 根因分析

### 3.1 问题代码（修复前）

```cpp
void MateTerminalMenuPlugin::openTerminal(){
    QUrl url = m_uri;
    auto directory = url.path().toUtf8().constData();   // <-- BUG
    gchar **argv = nullptr;
    g_shell_parse_argv (terminal_cmd.toUtf8().constData(), nullptr, &argv, nullptr);
    GError *err = nullptr;
    g_spawn_async (directory,        // <-- 使用已释放的指针
                   argv, nullptr, G_SPAWN_SEARCH_PATH, nullptr, nullptr, nullptr, &err);
    ...
}
```

`url.path().toUtf8()` 返回一个**临时 QByteArray**，`constData()` 返回其内部缓冲区指针；
`auto directory = ...` 这条语句结束时，临时对象被析构、堆缓冲区被 free，
`directory` 成为**悬空指针**。随后 `g_shell_parse_argv()` 的堆分配（恰好复制了字符串
`"mate-terminal"`）极可能复用这块刚释放的内存，于是传给 `g_spawn_async()` 的
"工作目录"变成了乱数据 —— harness 抓到的真实报错：

```
openTerminal failed: 更改到目录"mate-terminal"失败(没有那个文件或目录)
```

即 chdir 目标竟然是字符串 `mate-terminal` —— 与上述内存复用机制完全吻合。
chdir 失败 → g_spawn_async 返回 FALSE → 3.26 版本无兜底、无提示 → 用户看到"点了没反应"。

### 3.2 为什么"有的目录能开、有的不能"、"跨版本一直存在"

- 目标路径长度决定 `toUtf8()` 缓冲区落在哪个 glibc 堆尺寸桶（tcache bin）；
  该桶在语句结束到 `g_spawn_async()` 之间是否被后续小分配复用，决定了路径字符串是否被破坏。
  因此**对同一系统同一目录行为稳定，对不同路径（长度）结果不同**。
- 具体分配序列随 peony/Qt/glib 版本和进程堆历史变化，所以各 Kylin 版本上"哪些目录失败"不一样，
  但 bug 本身代代相传（**上游 GitHub ukui/peony-extensions master 分支至今仍是同样写法**）。
- 实测（harness，每路径触发 N 次）：旧插件在 `/tmp` 20/20 成功（路径太短不触发复用）、
  `/home/new211` 14/20 成功（间歇失败）、`/home/new211/peony/peony-3.2.3` 0/40（必现失败）。
- PCManFM / Nautilus 不经过这段代码，它们以各自正确的方式传工作目录，因此从未失败。

## 4. 修复方案

修改文件：`peony-extensions-3.2.3/peony-menu-plugin-mate-terminal/mate-terminal-menu-plugin.cpp`

要点：
1. **让 QByteArray 在 `g_spawn_async()` 调用期间存活**（根治 UAF）；
2. `g_shell_parse_argv` 失败/返回空 argv 时兜底为 `mate-terminal`；
3. 恢复并修复 `tryOpenAgain()` 兜底（主路径失败时用
   `QProcess::startDetached("mate-terminal", {"--working-directory", path})` 重试，
   仍失败则弹 QMessageBox 提示，而不是无声无息）；去掉对已 detach 进程无意义的
   `waitForFinished(-1)`。

### 4.1 Diff

```diff
--- a/mate-terminal-menu-plugin.cpp
+++ b/mate-terminal-menu-plugin.cpp
 #include <QtConcurrent>
 #include <QProcess>
+#include <QMessageBox>

 void MateTerminalMenuPlugin::openTerminal(){

     //qDebug()<<"triggered"<<uri;
     QUrl url = m_uri;
-    auto directory = url.path().toUtf8().constData();
+    //the QByteArray returned by toUtf8() must outlive the g_spawn_async() call,
+    //otherwise g_spawn_async() gets a dangling pointer as working directory and
+    //the spawn fails silently (bug: open terminal in right click menu does nothing).
+    QByteArray dirData = url.path().toUtf8();
+    const char *directory = dirData.constData();
     gchar **argv = nullptr;
-    g_shell_parse_argv (terminal_cmd.toUtf8().constData(), nullptr, &argv, nullptr);
+    if (!g_shell_parse_argv (terminal_cmd.toUtf8().constData(), nullptr, &argv, nullptr) || !argv) {
+        const gchar *fallback[] = {"mate-terminal", nullptr};
+        argv = g_strdupv (const_cast<gchar**>(fallback));
+    }
     GError *err = nullptr;
     g_spawn_async (directory, argv, nullptr, G_SPAWN_SEARCH_PATH,
                    nullptr, nullptr, nullptr, &err);
     if (err) {
-        qDebug()<<err->message;
+        qDebug()<<"openTerminal failed:"<<err->message;
         g_error_free(err);
         err = nullptr;
         //try again to open terminal
         //maybe this method is better,but still need more test to confirm
         tryOpenAgain();
     }
     g_strfreev (argv);
 }

 void MateTerminalMenuPlugin::tryOpenAgain()
 {
     QUrl url = m_uri;
     auto absPath = url.path();
     qDebug() << "tryOpenAgain terminal url:" <<absPath;
     QProcess p;
     p.setProgram(terminal_cmd);
     p.setArguments(QStringList()<<"--working-directory"<<absPath);
-//    p.startDetached();
-    p.startDetached(p.program(), p.arguments());
-    p.waitForFinished(-1);
+    bool result = QProcess::startDetached(p.program(), p.arguments());
+
+    if (!result) {
+        QMessageBox::critical(nullptr, tr("Open terminal fail"),
+                              tr("Open terminal failed, did you removed the default terminal?"
+                                 " If it's true please reinstall it."));
+    }
 }
```

### 4.2 构建与部署（本机实际执行的步骤）

```bash
# 依赖（一次性）
sudo apt-get install -y qtbase5-dev libpeony-dev

# 编译（在 peony-extensions 源码树外复制一份，common.json 需与源码同目录）
cp -r peony-extensions-3.2.3/peony-menu-plugin-mate-terminal /tmp/plugin-build
cp peony-extensions-3.2.3/common.json /tmp/plugin-build/
cd /tmp/plugin-build
qmake peony-menu-plugin-mate-terminal.pro "CONFIG-=debug" "CONFIG+=release"
make -j4
strip --strip-unneeded libpeony-menu-plugin-mate-terminal.so

# 部署（先备份原版）
DST=/usr/lib/x86_64-linux-gnu/peony-extensions
sudo cp -a $DST/libpeony-menu-plugin-mate-terminal.so $DST/libpeony-menu-plugin-mate-terminal.so.kylin-bak-20261025
sudo cp libpeony-menu-plugin-mate-terminal.so $DST/
sudo chown root:root $DST/libpeony-menu-plugin-mate-terminal.so && sudo chmod 644 $DST/libpeony-menu-plugin-mate-terminal.so

# 重启 peony 加载新插件（peony-qt-desktop 桌面进程不受影响）
killall peony && peony /home/new211/peony/peony-3.2.3 &
```

回滚：`sudo cp $DST/libpeony-menu-plugin-mate-terminal.so.kylin-bak-20261025 $DST/libpeony-menu-plugin-mate-terminal.so && killall peony`

## 5. 验证结果

### 5.1 无头 A/B 测试（同一加载器、同一触发流程，仅插件 .so 不同）

| 插件 | 触发路径 | 触发次数 | 成功 spawn | 工作目录正确 |
|---|---|---|---|---|
| 旧（系统 3.26） | /home/new211/peony/peony-3.2.3 | 40 | **0** | - |
| 旧（系统 3.26） | /home/new211 | 20 | 14 | 14 |
| 旧（系统 3.26） | /tmp | 20 | 20 | 20 |
| **新（修复版）** | /home/new211/peony/peony-3.2.3 | 40 | **40** | **40** |
| **新（修复版）** | /home/new211/peony | 20 | **20** | **20** |
| **新（修复版）** | /home/new211 | 20 | **20** | **20** |
| **新（修复版）** | /tmp | 20 | **20** | **20** |

旧插件抓到的典型报错（即 §3.1 的内存复用证据）：
`openTerminal failed: 更改到目录"mate-terminal"失败(没有那个文件或目录)`

### 5.2 真实桌面端到端验证

部署修复版并重启 peony 后，在 `/home/new211/peony/peony-3.2.3` 窗口内右键点击"打开终端"：
- 终端窗口立即弹出，标题 `new211@new211-pc: ~/peony/peony-3.2.3`；
- 该终端内 shell 进程的 `/proc/<pid>/cwd` 实测为 `/home/new211/peony/peony-3.2.3`（目录正确）；
- 用户人工反复测试确认恢复正常。

> 附注：mate-terminal 1.24 为单进程多窗口（factory）模型——新调用的 mate-terminal
> 会把"开新窗口"请求转给已运行实例后立即退出，因此验证时应看窗口/标题/shell 的 cwd，
> 而不是找"新 mate-terminal 进程"。

## 6. 测试工具（可复用）

- **A/B harness**：`QPluginLoader` 加载 .so → `menuActions(DirectoryView, uri, {})` →
  对返回的 QAction `trigger()`；两次触发之间做随机堆分配/释放模拟真实堆状态；
  `QT_QPA_PLATFORM=offscreen` 运行，不干扰桌面。
- **假终端**：把记录脚本放到 PATH 前部即可统计 spawn 成功率和实际 cwd：
  ```bash
  # /tmp/fakebin/mate-terminal
  #!/bin/bash
  echo "$PWD | $*" >> /tmp/spawned_markers.txt
  ```
  运行：`PATH=/tmp/fakebin:$PATH /tmp/harness/harness <插件.so> file:///某路径 40`

## 7. 注意事项

1. **系统升级会覆盖本修复**：`apt` 升级 `peony-open-terminal` 包会重装官方 .so（官方包内
   仍是带 bug 的版本）。如复发，按 §4.2 重新编译部署即可；原版备份
   `libpeony-menu-plugin-mate-terminal.so.kylin-bak-20261025` 保留在插件目录。
2. 上游 `ukui/peony-extensions`（含 Debian 分支/master）**至今未修复此 bug**，建议将本文
   提交给上游（4 行核心改动，见 §4.1 diff 中的 `dirData` 部分）。
3. 修复针对的是"工作目录悬空指针"这一根本问题；顺带恢复了旧版被移除的失败兜底和失败提示，
   今后即使出现其他失败（如终端被卸载）也会弹窗告知而不是无声无息。
