Skip to content

Don't follow symlinks when writing htop_history#1945

Open
Explorer09 wants to merge 1 commit into
htop-dev:mainfrom
Explorer09:history-nofollow
Open

Don't follow symlinks when writing htop_history#1945
Explorer09 wants to merge 1 commit into
htop-dev:mainfrom
Explorer09:history-nofollow

Conversation

@Explorer09

@Explorer09 Explorer09 commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Prevent a symlink attack that allows an attacker to empty any file a user has write permission to.

@Explorer09 Explorer09 force-pushed the history-nofollow branch 2 times, most recently from 634f8d0 to 17c1609 Compare April 6, 2026 09:54
@fasterit

fasterit commented Apr 6, 2026

Copy link
Copy Markdown
Member

What is a realistic attack scenario here?

@Explorer09

Copy link
Copy Markdown
Contributor Author

What is a realistic attack scenario here?

The attack is possible when the directory that the htoprc file resides is writable by the public or a local group. Then, suppose Mallory has write access to the directory, and he ln -s /home/alice/some_important_data htop_history to create a symlink. He can then wait for Alice to load that htoprc file and get hit.

If Alice runs htop with sudo privilege, things can get worse. I think you can get the idea.

@fasterit

fasterit commented Apr 6, 2026

Copy link
Copy Markdown
Member

The same is true for htoprc.

$ htop
Configuration /home/daniel/.config/htop/htoprc was resolved to /etc/passwd
Cannot save configuration to /etc/passwd: Permission denied

@Explorer09

Copy link
Copy Markdown
Contributor Author

The same is true for htoprc.

$ htop
Configuration /home/daniel/.config/htop/htoprc was resolved to /etc/passwd
Cannot save configuration to /etc/passwd: Permission denied

Gee. You motivated me to write this: #1947

@Explorer09 Explorer09 force-pushed the history-nofollow branch 2 times, most recently from 1d87942 to 6cc06f7 Compare April 7, 2026 11:07
@Explorer09

Copy link
Copy Markdown
Contributor Author

For your info, the Settings_resolveSymlink function I introduced in #1947 can be easily adapted to work with htop_history file, too. If you people want it.
(The function adds a simple safeguard for symbolic links by checking the ownership of the link. It follows the symlink only if the link is owned by either root or the EUID running the htop instance.)

@Explorer09 Explorer09 force-pushed the history-nofollow branch 3 times, most recently from f861efd to 862b812 Compare April 16, 2026 16:18
@Explorer09 Explorer09 force-pushed the history-nofollow branch 2 times, most recently from 8494ba8 to 1c6cf78 Compare April 19, 2026 17:52
@Explorer09 Explorer09 force-pushed the history-nofollow branch 3 times, most recently from b029b24 to 597c75a Compare May 1, 2026 10:01
@Explorer09 Explorer09 force-pushed the history-nofollow branch 3 times, most recently from a77f203 to 91d43cc Compare May 11, 2026 04:08
@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c48c4669-5b07-47a3-82e9-dcf21ad95018

📥 Commits

Reviewing files that changed from the base of the PR and between a735f11 and be3db57.

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

📝 Walkthrough

Security Fix: Prevent Symlink Attack in History File Writing

Adds the O_NOFOLLOW flag to the open() call in History_save() to prevent following symlinks when writing the htop history file. This blocks a symlink attack where an attacker with write access to the history file's directory could create a symlink pointing to an arbitrary file, causing htop to overwrite or empty files the user has permission to write.

Implementation: Single-line change adding O_NOFOLLOW to the flags on line 72 of History.c alongside the existing O_WRONLY | O_CREAT | O_TRUNC options. The open() call will now fail (returning -1) if the target is a symlink, and the function returns early without writing. The remaining save flow—fdopen, entry writing, and error handling—is unchanged.

Solution quality: Minimal, focused, and directly effective. The fix uses a standard POSIX flag designed precisely for this threat model. The approach aligns with htop's existing practices (noted in the code comment referencing Settings_write which uses a different temp-file-and-rename strategy for configuration files).

Walkthrough

This PR hardens history file persistence in htop by adding the O_NOFOLLOW flag to the open() call in History_save(). The flag prevents the history file path from resolving to a symlink target, blocking potential privilege escalation or data manipulation. The change is a single flag addition to the existing open() invocation, leaving all downstream logic—file descriptor wrapping, entry writing, and error handling—unchanged.

Poem

A symlink's trap, now sealed away,
History files, the safer way,
O_NOFOLLOW stands and firmly blocks,
One flag secures the history box.


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: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 80340a99-d27d-4077-83c9-02aa57f43880

📥 Commits

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

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

Comment thread History.c
return;
/* Settings_write writes things via a temp file & rename, we do it less robust but faster here: */
int fd = open(this->filename, O_WRONLY | O_CREAT | O_TRUNC, 0600);
int fd = open(this->filename, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0600);

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 | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for Settings_resolveSymlink function definition
ast-grep --pattern $'Settings_resolveSymlink($$$) {
  $$$
}'

# Also search for its usage and declaration
rg -n 'Settings_resolveSymlink' --type=c -C3

Repository: htop-dev/htop

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Find O_NOFOLLOW usage to check for platform guards or fallbacks
rg -n 'O_NOFOLLOW' --type=c -C2

# Check for any HAVE_O_NOFOLLOW or similar feature macros
rg -n 'HAVE.*NOFOLLOW|O_NOFOLLOW.*ifdef' --type=c

Repository: htop-dev/htop

Length of output: 2132


🏁 Script executed:

cat -n History.c | head -100

Repository: htop-dev/htop

Length of output: 3290


History_load() also needs O_NOFOLLOW for consistency.

History_load() uses fopen() (line 25), which follows symlinks, while History_save() now uses open() with O_NOFOLLOW (line 72), preventing symlink following. A user with an intentional symlink will see history loaded successfully but updates silently fail to persist, causing unexpected data loss.

Apply O_NOFOLLOW to History_load() as well by switching from fopen() to open() with appropriate flags. This matches the existing pattern in Settings.c (line 326), which already uses O_NOFOLLOW for file access.

Prevent a symlink attack that allows an attacker to empty any file a
user has write permission to.

Signed-off-by: Kang-Che Sung <explorer09@gmail.com>
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