Skip to content

Restrict htoprc symlink resolution with owner check#1947

Open
Explorer09 wants to merge 1 commit into
htop-dev:mainfrom
Explorer09:settings-symlink
Open

Restrict htoprc symlink resolution with owner check#1947
Explorer09 wants to merge 1 commit into
htop-dev:mainfrom
Explorer09:settings-symlink

Conversation

@Explorer09

@Explorer09 Explorer09 commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Restrict the possibility of a symlink attack on the htoprc file. If the htoprc file to be read is a symlink, only resolve the link if it's owned by the same EUID or root user (UID 0).

@Explorer09 Explorer09 force-pushed the settings-symlink branch 6 times, most recently from 4043a07 to 7c38b17 Compare April 13, 2026 17:51
@Explorer09 Explorer09 force-pushed the settings-symlink branch 3 times, most recently from 5b96667 to c465f9c Compare April 19, 2026 17:52
@Explorer09 Explorer09 force-pushed the settings-symlink branch 3 times, most recently from 708d953 to 168431a Compare May 1, 2026 10:01
@Explorer09 Explorer09 force-pushed the settings-symlink branch 3 times, most recently from d59a397 to a32f17e Compare May 11, 2026 04:08
@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Implementation

Adds security restriction on symlink resolution for htoprc configuration files by introducing a static Settings_resolveSymlink() helper function. The function opens paths with O_NOFOLLOW to prevent symlink traversal, then uses fstat() to inspect file metadata. Symlinks are only resolved if owned by the effective UID or root (UID 0); untrusted symlinks are returned as-is. Resolution uses readlinkat() to read the target and realpath() for canonical path conversion, with proper handling of relative symlink targets via intermediate path construction.

Code Quality

The implementation is well-structured with portable flag handling (O_EXEC, O_PATH checked at compile time), EINTR-safe open call loop, and clear error handling via labeled cleanup paths. Validates symlink size against PATH_MAX before reading. Correctly duplicates output strings for caller management.

Commit Structure

Single focused commit with clear security-oriented message and proper sign-off. Directly integrates the new function into Settings_new(), replacing prior unconditional symlink resolution logic. The change is localized and minimal in scope.

Security Assessment

Directly addresses symlink attack vectors by enforcing owner verification before resolution. Allows read-only access to untrusted symlink paths themselves while preventing redirection to unintended targets. Complements existing file writability checks (O_RDWR flag in Settings_write()).

Walkthrough

Settings.c adds Settings_resolveSymlink, which opens a path without following symlinks using O_NOFOLLOW and inspects file type and ownership via fstat. Symlinks owned by root or the effective user are treated as trusted; their targets are read with readlinkat and resolved to absolute paths using realpath. Failures yield an allocated empty string; non-symlinks return a duplicate of the original path. Settings_new now uses this helper to initialize this->filename, replacing the previous PATH_MAX allocation and realpath fallback.

Poem

Paths once blind now ask who holds the key,
A careful open, a stat to guarantee.
Trusted links step forward, others stay still,
readlinkat and realpath shape the final will.
Settings bind their names with ownership and care.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 84a85ff8-9817-4fc4-810a-9aed50c08c82

📥 Commits

Reviewing files that changed from the base of the PR and between b7f9df9 and f2168a1.

📒 Files selected for processing (1)
  • Settings.c

Comment thread Settings.c
Comment thread Settings.c
Comment thread Settings.c

@coderabbitai coderabbitai 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.

♻️ Duplicate comments (1)
Settings.c (1)

844-852: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the path separator when rebuilding relative symlink targets.

Line 847 computes the prefix length without the trailing /, so a link like /home/u/.config/htop/htoprc -> ../shared/htoprc is rebuilt as /home/u/.config/htop../shared/htoprc. That makes realpath() fail and breaks valid trusted relative symlinks.

Proposed fix
    size_t dirnameLen = 0;
    if (buf[0] != '/') {
       const char *lastSlash = strrchr(path, '/');
-      dirnameLen = lastSlash ? (size_t)(lastSlash - (const char*)path) : 0;
+      dirnameLen = lastSlash ? (size_t)(lastSlash - (const char*)path) + 1 : 0;
    }
    char* intermediatePath = xMalloc(dirnameLen + (size_t)len + 1);
    memcpy(intermediatePath, path, dirnameLen);
    memcpy(intermediatePath + dirnameLen, buf, (size_t)len + 1);

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 59946287-827d-4e13-b881-d5a07878c079

📥 Commits

Reviewing files that changed from the base of the PR and between f2168a1 and ee7832c.

📒 Files selected for processing (1)
  • Settings.c

Restrict the possibility of a symlink attack on the htoprc file.
If the htoprc file to be read is a symlink, only resolve the link if
it's owned by the same EUID or root user (UID 0).

Signed-off-by: Kang-Che Sung <explorer09@gmail.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 330fe679-9b51-4413-8e08-559edf3106ac

📥 Commits

Reviewing files that changed from the base of the PR and between c0df87f and 1935d54.

📒 Files selected for processing (1)
  • Settings.c

Comment thread Settings.c
Comment on lines +811 to +817
#ifdef O_PATH
// O_PATH is specific to Linux and FreeBSD.
openFlags |= O_PATH;
#endif
do {
fd = open(path, openFlags);
} while (fd < 0 && errno == EINTR);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect supported targets and existing feature checks related to O_PATH/readlinkat.
# Expectation: every supported target either supports opening symlinks by fd plus readlinkat(fd, ""),
# or the PR adds an intentional platform-specific fallback/guard.

set -euo pipefail

printf '\n== Build/platform references ==\n'
rg -n -C3 '\b(linux|freebsd|openbsd|netbsd|darwin|macos|solaris|bsd)\b' \
   -g '!**/build/**' -g '!**/dist/**' -g '!**/node_modules/**' || true

printf '\n== Feature checks and direct usage ==\n'
rg -n -C4 '\b(O_PATH|O_EXEC|O_NOFOLLOW|readlinkat|realpath|lstat|readlink)\b' \
   -g '!**/build/**' -g '!**/dist/**' -g '!**/node_modules/**' || true

printf '\n== Candidate build/config files ==\n'
fd -a 'configure|configure.ac|meson.build|CMakeLists.txt|Makefile|ci|workflow|Dockerfile|README|INSTALL' . || true

Repository: htop-dev/htop

Length of output: 50369


🏁 Script executed:

cat -n Settings.c | sed -n '800,870p'

Repository: htop-dev/htop

Length of output: 2414


🏁 Script executed:

rg -n -A 5 -B 5 'readlinkat|O_PATH|O_EXEC|O_NOFOLLOW' Settings.c

Repository: htop-dev/htop

Length of output: 1345


🏁 Script executed:

rg -n '`#ifdef`|`#if` defined' Settings.c | head -30

Repository: htop-dev/htop

Length of output: 369


🏁 Script executed:

# Search for any readlinkat-related comments or conditionals that may indicate platform issues
rg -n 'readlinkat|empty.?path' --type-list | head -20

Repository: htop-dev/htop

Length of output: 447


🏁 Script executed:

# Check if there are any FreeBSD, OpenBSD, NetBSD, Darwin, or Solaris specific workarounds
rg -B5 -A5 'noPath|readlinkat' Settings.c

Repository: htop-dev/htop

Length of output: 1073


🏁 Script executed:

# Check build/configure for readlinkat checks
rg -n 'readlinkat|AC_CHECK' configure.ac | head -20

Repository: htop-dev/htop

Length of output: 1087


🌐 Web query:

readlinkat empty path "" support OpenBSD NetBSD Darwin Solaris

💡 Result:

Support for passing an empty string ("") as the path argument to readlinkat varies significantly across operating systems. On Linux, readlinkat explicitly supports an empty string path argument, provided that the AT_EMPTY_PATH flag is used or the kernel version is 2.6.39 or later [1][2][3][4]. In this specific case, the call operates directly on the symbolic link referred to by the file descriptor (dirfd), which must have been opened appropriately (typically with O_PATH and O_NOFOLLOW) [1][3]. Conversely, on OpenBSD, NetBSD, Darwin (macOS), and according to the POSIX standards, passing an empty string as a path argument to readlinkat is not supported and will result in an error [5][6][7][8][9]. POSIX specifically defines that readlinkat shall fail with ENOENT if the path argument is an empty string [9]. Similarly, these BSD-derived systems do not implement the Linux-specific AT_EMPTY_PATH extension, meaning standard path resolution rules apply where an empty string is treated as an invalid path name [5][6][10]. Summary: - Linux: Supported (requires specific flags/kernel version) [1][2][3]. - OpenBSD, NetBSD, Darwin, POSIX: Not supported (returns error, typically ENOENT) [5][6][7][8][9].

Citations:


Symlink resolution will fail silently on FreeBSD, OpenBSD, NetBSD, and Darwin.

The readlinkat(fd, "") call at line 840 is Linux-specific and unsupported on FreeBSD, OpenBSD, NetBSD, Darwin, and Solaris—it will return ENOENT. When this fails, the function falls back to returning an empty string (line 866), which breaks trusted symlink config resolution on those platforms without any diagnostic. Add #ifdef HAVE_DECL_READLINKAT_EMPTY_PATH or platform-specific guards to fall back to an alternative resolution method (e.g., readlink() if symlink was already opened with O_PATH) on non-Linux systems, or document this as a known limitation.

Evidence

Line 840 uses readlinkat(fd, "", buf, PATH_MAX) unconditionally. According to POSIX and BSD implementations, an empty string path argument is invalid and causes ENOENT. This is a Linux-specific extension not available on FreeBSD, OpenBSD, NetBSD, Darwin, or Solaris.

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.

1 participant