Skip to content

fix: fix TOCTOU race in wallpaper save function#1145

Open
xionglinlin wants to merge 1 commit into
linuxdeepin:masterfrom
xionglinlin:master
Open

fix: fix TOCTOU race in wallpaper save function#1145
xionglinlin wants to merge 1 commit into
linuxdeepin:masterfrom
xionglinlin:master

Conversation

@xionglinlin

@xionglinlin xionglinlin commented Jun 12, 2026

Copy link
Copy Markdown
Contributor
  1. Use unix.Open with O_NOFOLLOW to atomically reject symlinks, eliminating TOCTOU race condition
  2. Reuse the file descriptor for stat, MD5 checksum calculation, and content reading
  3. Compute MD5 from already-read content instead of re-opening the file
  4. Replace runuser/cat shell command with direct file read via the file descriptor
  5. Close file descriptor properly with defer

Log: Fixed security vulnerability in wallpaper saving functionality

Influence:

  1. Test saving wallpaper with a regular file to verify functionality
  2. Test with a symlink target to confirm symlinks are rejected
  3. Verify MD5 checksum is computed correctly
  4. Test file permission handling and error cases
  5. Verify the fix works for both regular and solid color wallpapers
  6. Test concurrent operations to ensure race condition is mitigated

fix: 修复壁纸保存函数中的TOCTOU竞态漏洞

  1. 使用unix.Open函数配合O_NOFOLLOW标志原子性地拒绝符号链接,消除TOCTOU竞 态条件
  2. 复用文件描述符进行stat、MD5校验和计算及内容读取
  3. 从已读取的内容计算MD5,避免重新打开文件
  4. 使用文件描述符直接读取替换runuser/cat shell命令
  5. 使用defer正确关闭文件描述符

Log: 修复壁纸保存功能的安全漏洞

Influence:

  1. 使用常规文件测试保存壁纸功能,验证功能正常
  2. 使用符号链接目标测试,确认符号链接被拒绝
  3. 验证MD5校验和计算是否正确
  4. 测试文件权限处理和错误情况
  5. 验证修复对常规壁纸和纯色壁纸均有效
  6. 测试并发操作以确保竞态条件已被解决

PMS: BUG-364751
Change-Id: I0ed2d3474f0a869e4f61731f53730f6103720785

Summary by Sourcery

Mitigate a TOCTOU race and improve security in the custom wallpaper saving path by hardening file handling and checksum computation.

Bug Fixes:

  • Prevent TOCTOU-based symlink attacks in the wallpaper save function by atomically rejecting symlinks and avoiding re-open races.

Enhancements:

  • Reuse a single file descriptor for stat, content reading, and MD5 computation, and replace an external runuser/cat invocation with direct file reads to simplify and harden the code.

1. Use unix.Open with O_NOFOLLOW to atomically reject symlinks,
eliminating TOCTOU race condition
2. Reuse the file descriptor for stat, MD5 checksum calculation, and
content reading
3. Compute MD5 from already-read content instead of re-opening the file
4. Replace runuser/cat shell command with direct file read via the file
descriptor
5. Close file descriptor properly with defer

Log: Fixed security vulnerability in wallpaper saving functionality

Influence:
1. Test saving wallpaper with a regular file to verify functionality
2. Test with a symlink target to confirm symlinks are rejected
3. Verify MD5 checksum is computed correctly
4. Test file permission handling and error cases
5. Verify the fix works for both regular and solid color wallpapers
6. Test concurrent operations to ensure race condition is mitigated

fix: 修复壁纸保存函数中的TOCTOU竞态漏洞

1. 使用unix.Open函数配合O_NOFOLLOW标志原子性地拒绝符号链接,消除TOCTOU竞
态条件
2. 复用文件描述符进行stat、MD5校验和计算及内容读取
3. 从已读取的内容计算MD5,避免重新打开文件
4. 使用文件描述符直接读取替换runuser/cat shell命令
5. 使用defer正确关闭文件描述符

Log: 修复壁纸保存功能的安全漏洞

Influence:
1. 使用常规文件测试保存壁纸功能,验证功能正常
2. 使用符号链接目标测试,确认符号链接被拒绝
3. 验证MD5校验和计算是否正确
4. 测试文件权限处理和错误情况
5. 验证修复对常规壁纸和纯色壁纸均有效
6. 测试并发操作以确保竞态条件已被解决

PMS: BUG-364751
Change-Id: I0ed2d3474f0a869e4f61731f53730f6103720785
@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: xionglinlin

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sourcery-ai

sourcery-ai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Refactors the wallpaper-saving path to safely open the source file using unix.Open with O_NOFOLLOW, reuse the resulting file descriptor for stat, content read, and MD5 hashing, and remove the runuser/cat shell pipeline, thereby fixing a TOCTOU race and tightening permissions handling.

Sequence diagram for updated SaveCustomWallPaper file handling

sequenceDiagram
    participant Daemon
    participant SaveCustomWallPaper
    participant unix
    participant osFile
    participant io
    participant md5
    participant os

    Daemon->>SaveCustomWallPaper: SaveCustomWallPaper(sender, username, file)
    SaveCustomWallPaper->>unix: Open(file, O_RDONLY|O_NOFOLLOW|O_CLOEXEC, 0)
    unix-->>SaveCustomWallPaper: fd
    SaveCustomWallPaper->>osFile: NewFile(uintptr(fd), file)
    SaveCustomWallPaper->>osFile: Stat()
    osFile-->>SaveCustomWallPaper: FileInfo
    SaveCustomWallPaper->>io: ReadAll(osFile)
    io-->>SaveCustomWallPaper: src
    SaveCustomWallPaper->>md5: Sum(src)
    md5-->>SaveCustomWallPaper: md5sum
    SaveCustomWallPaper->>os: WriteFile(destFile, src, 0644)
    os-->>SaveCustomWallPaper: err
    SaveCustomWallPaper-->>Daemon: destFile / error
Loading

File-Level Changes

Change Details Files
Harden wallpaper source file handling by opening via unix.Open with O_NOFOLLOW and reusing the resulting file descriptor for subsequent operations.
  • Import golang.org/x/sys/unix to access unix.Open and related flags.
  • Replace os.Stat(file) with unix.Open using O_RDONLY
O_NOFOLLOW
Compute MD5 and copy wallpaper contents directly from the opened file instead of re-opening or shelling out as another user.
  • Read the entire file content via io.ReadAll(f) once and reuse the resulting byte slice.
  • Compute the MD5 checksum from the in-memory content using md5.Sum and fmt.Sprintf instead of dutils.SumFileMd5.
  • Remove the runuser/cat exec pipeline and rely on the already-read content as the source for os.WriteFile.
  • Preserve existing destination path logic and write the content to destFile with os.WriteFile(destFile, src, 0644).
bin/dde-system-daemon/wallpaper.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

<style> .ai-report h1 { color: #2c3e50; border-bottom: 2px solid #2c3e50; padding-bottom: 10px; font-size: 24px; } .ai-report h2 { color: #2980b9; margin-top: 25px; font-size: 20px; } .ai-report h3 { color: #34495e; font-size: 16px; } .ai-report .solution-box { background-color: #f8f9fa; padding: 15px; border-left: 4px solid #28a745; margin-top: 10px; } .ai-report .info-needed { background-color: #fff3cd; padding: 15px; border-left: 4px solid #ffc107; margin-top: 10px; } .ai-report .note { background-color: #e7f4ff; padding: 15px; border-left: 4px solid #17a2b8; margin-top: 10px; } .ai-report code { background-color: #f1f2f3; padding: 2px 4px; border-radius: 3px; color: #d63384; font-family: monospace; } </style>

问题分析报告

问题分析

dde-system-daemon 项目的 SaveCustomWallPaper 函数中存在严重的 TOCTOU 竞态条件 漏洞与符号链接跟随安全风险。原代码先通过 os.Stat(file) 检查文件状态,随后通过 exec.Command("runuser", ... "cat", file) 读取文件内容。在这两步操作之间存在时间窗口,攻击者可利用符号链接将 file 动态替换为指向敏感文件(如 /etc/shadow)的链接,从而绕过检查实现越权读取。

解决方案

  1. 原子性拒绝符号链接:引入 golang.org/x/sys/unix 包,使用 unix.Open 配合 O_NOFOLLOWO_CLOEXEC 标志打开文件,在内核层面原子性地拒绝跟随符号链接。随后通过 os.NewFile 将获取到的文件描述符封装为 *os.File 对象。
  2. 消除 TOCTOU 竞态:将后续的文件状态检查 f.Stat() 与文件内容读取 io.ReadAll(f) 统一基于上述已安全打开的文件描述符 f 进行,彻底消除检查与使用之间的时间差攻击窗口。
  3. 移除不安全的外部命令调用:删除原有的 exec.Command("runuser", "-u", username, "--", "cat", file) 逻辑,改为直接使用 Go 原生的 io.ReadAll(f) 从文件描述符读取内容至 src 变量,既提升了执行性能又堵住了安全漏洞。
  4. 优化 MD5 计算逻辑:引入 crypto/md5 包,直接对已读取至内存的 src 字节切片执行 md5.Sum(src) 计算,避免原代码中 dutils.SumFileMd5(file) 可能引发的二次打开文件风险。

补充说明

在系统级守护进程开发中,应严格遵循“打开即校验”原则,避免将文件路径在不同系统调用或外部命令之间传递。对于需要权限控制的文件读取操作,应优先考虑基于已打开的文件描述符进行权限降级或校验,而非依赖不安全的衍生进程命令。

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • By replacing the runuser/cat call with a direct read from the daemon process, the effective permissions check has changed; consider whether the daemon should still enforce that username has read access to the source file (e.g., via a dedicated helper or explicit ACL check) to avoid unintentionally broadening access to files.
  • The new approach reads the entire wallpaper file into memory to compute MD5 and then writes it out; if wallpapers can be large, consider switching to a streaming hash and copy (e.g., using io.Copy with md5.New) to avoid holding the whole file in memory at once.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- By replacing the `runuser`/`cat` call with a direct read from the daemon process, the effective permissions check has changed; consider whether the daemon should still enforce that `username` has read access to the source file (e.g., via a dedicated helper or explicit ACL check) to avoid unintentionally broadening access to files.
- The new approach reads the entire wallpaper file into memory to compute MD5 and then writes it out; if wallpapers can be large, consider switching to a streaming hash and copy (e.g., using `io.Copy` with `md5.New`) to avoid holding the whole file in memory at once.

## Individual Comments

### Comment 1
<location path="bin/dde-system-daemon/wallpaper.go" line_range="225-228" />
<code_context>
 	}
-	info, err := os.Stat(file)
+	// Use O_NOFOLLOW to atomically reject symlinks, eliminating TOCTOU race.
+	fd, err := unix.Open(file, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
+	if err != nil {
+		logger.Warning(err)
+		return "", dbusutil.ToError(err)
+	}
+	f := os.NewFile(uintptr(fd), file)
</code_context>
<issue_to_address>
**🚨 issue (security):** Opening the file as the daemon user changes the security model compared to the previous `runuser`-based read.

With the prior `runuser` approach, the kernel enforced the target user's filesystem permissions and returned "permission denied" when appropriate. Now the daemon opens the file itself, likely with broader privileges, so per-user filesystem checks are no longer applied at this layer. Please confirm this change is deliberate and that higher-level authorization fully replaces the lost OS‑level permission check for this path.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +225 to +228
fd, err := unix.Open(file, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
if err != nil {
logger.Warning(err)
return "", dbusutil.ToError(err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Opening the file as the daemon user changes the security model compared to the previous runuser-based read.

With the prior runuser approach, the kernel enforced the target user's filesystem permissions and returned "permission denied" when appropriate. Now the daemon opens the file itself, likely with broader privileges, so per-user filesystem checks are no longer applied at this layer. Please confirm this change is deliberate and that higher-level authorization fully replaces the lost OS‑level permission check for this path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants