From 402461dace6cb9cab474ff17ce6b604cba8a6a2d Mon Sep 17 00:00:00 2001 From: fuleyi Date: Wed, 26 Aug 2026 14:40:31 +0800 Subject: [PATCH] fix: harden udev rule file permission and durability on write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Replace os.Create with os.OpenFile(..., 0644) to avoid 0666 permission regression under a permissive umask 2. Add explicit f.Chmod(0644) to narrow permissions on existing files that may have been widened externally 3. fsync parent directory after first creation so the directory entry survives a forced power loss, not just file data 4. Repair file mode on the content-match fast path so existing rule files with widened permissions are still narrowed to 0644 5. Use O_EXCL to atomically detect first creation, avoiding the Stat/OpenFile TOCTOU race that could skip the parent directory sync when the file is removed concurrently 6. Truncate only after Chmod succeeds, so a Chmod failure cannot erase the previous valid rule content via O_TRUNC 7. Best-effort fsync the parent directory on the fast path to recover when a prior dir.Sync failed and the call is retried 8. Split setTouchpadEnableViaUdev into writeUdevRuleFile and syncDirBestEffort for readability without changing behavior Log: Fix touchpad udev rule file losing durability and permission guarantees introduced by the fsync refactor Influence: 1. Disable touchpad, power off forcibly, reboot and verify the udev rule file still exists and the touchpad stays disabled 2. Check the permission of /etc/udev/rules.d/90-dde-touchpad.rules is 0644 with a permissive umask (e.g. 0000) set on the service 3. Toggle touchpad enable/disable repeatedly and confirm no errors in dde-system-daemon logs fix: 加固触控板 udev 规则文件写入的权限与落盘耐久性 1. 用 os.OpenFile(..., 0644) 替换 os.Create,避免在宽松 umask 下 0666 导致规则文件可被组或全局写入 2. 新增 f.Chmod(0644) 兜底,收窄已被外部改宽的已存在文件权限 3. 首次创建文件后 fsync 父目录,确保目录项在断电时也能落盘 4. 内容匹配的 fast path 同样执行 Chmod(0644),修复已存在但权限 被改宽的规则文件跳过权限收窄的问题 5. 用 O_EXCL 原子探测首次创建,消除 Stat/OpenFile 之间文件被 并发删除导致 isNew 误判、父目录漏 sync 的 TOCTOU 竞态 6. 先 Chmod 成功再 Truncate,避免 Chmod 失败时 O_TRUNC 已清空 原有效规则内容 7. fast path 补 best-effort 父目录 fsync,覆盖上次创建时 dir.Sync 失败后重试永久跳过目录项持久化的场景 8. 将 setTouchpadEnableViaUdev 拆分为 writeUdevRuleFile 与 syncDirBestEffort,提升可读性,行为不变 Log: 修复 fsync 重构引入的触控板 udev 规则文件权限退化与 断电后目录项丢失问题 Influence: 1. 禁用触控板后强制断电重启,验证 udev 规则文件仍存在且触控板保持禁用 2. 在服务设置宽松 umask(如 0000)下检查 /etc/udev/rules.d/90-dde-touchpad.rules 权限为 0644 3. 反复启用/禁用触控板,确认 dde-system-daemon 日志无报错 PMS: BUG-374789 --- system/inputdevices1/touchpad.go | 111 ++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 25 deletions(-) diff --git a/system/inputdevices1/touchpad.go b/system/inputdevices1/touchpad.go index 172dc5c91..d4d36e3f0 100644 --- a/system/inputdevices1/touchpad.go +++ b/system/inputdevices1/touchpad.go @@ -143,41 +143,102 @@ func (t *Touchpad) setTouchpadEnableViaUdev(enabled bool) error { return err } logger.Info("removed udev rule file:", udevRuleFile) - } else { - // 禁用:生成动态的 udev 规则内容 - udevRuleContent := generateUdevRuleContent() - - // 检查文件是否已存在且内容相同 - existingContent, err := os.ReadFile(udevRuleFile) - if err == nil && string(existingContent) == udevRuleContent { - logger.Debug("udev rule file already exists with correct content, skip writing") - return nil - } + return t.refreshTouchpadDevices() + } - // 创建或覆盖 udev 规则文件,使用 fsync 确保落盘, - // 防止强制关机(断电)时 page cache 丢失导致规则文件丢失 - f, err := os.Create(udevRuleFile) - if err != nil { - return err + // 禁用:写入 udev 规则文件;若内容未变则无需刷新设备 + changed, err := writeUdevRuleFile(generateUdevRuleContent()) + if err != nil { + return err + } + if !changed { + return nil + } + return t.refreshTouchpadDevices() +} + +// writeUdevRuleFile 以 0644 权限原子地写入 udev 规则文件,并确保数据与目录项落盘。 +// 返回 changed 表示文件内容是否发生变化(true=已写入或新建,false=内容未变跳过写入)。 +// 安全要点: +// - fast path 内容已正确时仍收窄权限,防止外部改宽的 0666 等遗留 +// - 用 O_EXCL 原子探测是否为首次新建,避免 Stat/OpenFile TOCTOU 竞态 +// - 先 Chmod 成功再 Truncate,避免权限修改失败时 O_TRUNC 已清空原内容 +// - 文件数据 fsync 后,首次创建还需 fsync 父目录以保证目录项断电不丢 +func writeUdevRuleFile(udevRuleContent string) (changed bool, err error) { + // 内容已正确时,仅做权限兜底与父目录 best-effort sync 后直接返回 + if existingContent, rerr := os.ReadFile(udevRuleFile); rerr == nil && + string(existingContent) == udevRuleContent { + if err := os.Chmod(udevRuleFile, 0644); err != nil { + return false, err } - _, err = f.Write([]byte(udevRuleContent)) + syncDirBestEffort(filepath.Dir(udevRuleFile)) + logger.Debug("udev rule file already exists with correct content, skip writing") + return false, nil + } + + // 尝试以 O_EXCL 独占创建,原子判断是否为首次新建, + // 避免 Stat/OpenFile 之间因并发删除导致的 TOCTOU 竞态使父目录漏 sync + f, err := os.OpenFile(udevRuleFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) + isNew := err == nil + if err != nil && !os.IsExist(err) { + return false, err + } + if os.IsExist(err) { + // 文件已存在,以 O_WRONLY 打开但不立即截断 + f, err = os.OpenFile(udevRuleFile, os.O_WRONLY, 0644) if err != nil { - f.Close() - return err + return false, err } - err = f.Sync() + } + // 先收窄权限,成功后再截断写入,避免 Chmod 失败时 O_TRUNC 已清空原内容导致规则丢失 + if err := f.Chmod(0644); err != nil { + f.Close() + return false, err + } + if err := f.Truncate(0); err != nil { + f.Close() + return false, err + } + if _, err := f.Write([]byte(udevRuleContent)); err != nil { + f.Close() + return false, err + } + // fsync 确保文件数据落盘, + // 防止强制关机(断电)时 page cache 丢失导致规则文件丢失 + if err := f.Sync(); err != nil { + f.Close() + return false, err + } + if err := f.Close(); err != nil { + return false, err + } + // 首次创建文件时,还需 fsync 父目录以确保目录项落盘, + // 否则断电后文件数据虽已持久但目录项丢失,规则文件仍会缺失 + if isNew { + dir, err := os.Open(filepath.Dir(udevRuleFile)) if err != nil { - f.Close() - return err + return false, err } - err = f.Close() + err = dir.Sync() + dir.Close() if err != nil { - return err + return false, err } - logger.Info("created udev rule file:", udevRuleFile) } + logger.Info("created udev rule file:", udevRuleFile) + return true, nil +} - return t.refreshTouchpadDevices() +// syncDirBestEffort 尽力同步目录元数据,失败仅记录不返回错误。 +func syncDirBestEffort(dir string) { + d, err := os.Open(dir) + if err != nil { + return + } + defer d.Close() + if err := d.Sync(); err != nil { + logger.Warning("failed to sync parent directory:", err) + } } // reloadUdevRules 重新加载 udev 规则