Skip to content

Conversation

@mchlyu
Copy link

@mchlyu mchlyu commented Dec 25, 2025

Addresses #506

All input to script is given on stdin as a sequence of "PID" "USER" "COMMAND" in TSV format.
Users can configure htoprc to provide a script path setting that will act as fallback if no file exists at $XDG_CONFIG_HOME/htop/run_script.

I tested functionality using this gdb script on Ubuntu.

#!/bin/bash

# read input from stdin, expecting TSV formatted tuple [int, string, string]
while IFS=$'\t' read -r pid usr cmd; do
    gdb -p "$pid" -batch -ex "thread apply all bt" -ex "quit"
done

Created new InfoScreen pager heavily inspired by OpenFilesScreen and TraceScreen to display stdout and stderr from script.
Also updated help screen and man page to include this feature.

Valgrind did not report any new memory leaks.

I'm open to any suggestions for improving the structure, security, etc. of the code or clarity of the documentation.
This is my first contribution to this project (and open source in general), so please let me know if I’ve missed any conventions.

@Explorer09
Copy link
Contributor

There are a lot of feature suggestions of running arbitrary commands through htop, but almost all the pull request code suffers from a problem:

htop is often run with elevated privileges (e.g. sudo), and there is a need for (1) an indicator on whether the external program should be run with elevated privileges or dropped privileges, and (2) a security check that, if the script is meant to be run with elevated privileges, it's not installed by users other than root (which can prevent accidental running of malicious programs).

So this pull request doesn't have either security mechanism yet.

const char* path = String_cat(home, "/htop/run_script");
FILE* file = fopen(path, "r");
if (file) {
execl(path, path, NULL);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
execl(path, path, NULL);
execl(path, path, (void*)NULL);

Copy link
Contributor

Choose a reason for hiding this comment

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

One more thing. Since we need to verify the ownership of the run_script to be executed, the execl, execv family of API would not fit the job.

Consider the fexecve(3) or execveat(2) API instead.

Copy link
Author

Choose a reason for hiding this comment

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

Got it, thank you for the clarification and suggestions. I'll see if I'm able to implement something that fits #1844 to address this security issue.

} else {
// check if htoprc has something
const char* htoprc_path = st->host->settings->scriptLocation;
execl(htoprc_path, htoprc_path, NULL);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
execl(htoprc_path, htoprc_path, NULL);
execl(htoprc_path, htoprc_path, (void*)NULL);

Copy link
Member

@BenBE BenBE left a comment

Choose a reason for hiding this comment

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

Were parts of this PR assisted or written aided by LLMs?

Also, as the command line of a process may contain anything, A more robust solution would provide the exact length of the data to follow and push the data after that literal. Arguments are split by \0 by the kernel, but since programs can overwrite that buffer anything goes …

Also, given that there are some other (similar) screens, what do you think of merging these (in a separate) PR to reduce code duplication?

Comment on lines +38 to +39
char pid[pid_length + 1];
snprintf(pid, pid_length + 1, "%d", row->id);
Copy link
Member

Choose a reason for hiding this comment

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

No VLA. Also use internal xSnprintf APIs. Also, why not allocate a static buffer that's large enough for any %d like 32 bytes? Or use multiple writes for the target FD?

Copy link
Author

Choose a reason for hiding this comment

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

Ah I missed xSnprintf when I was going through xUtils, apologies.
I chose to do all these string concatenations and a singular write because I thought (although I have no data to back this up) a write is much more expensive than string operations, but I see how it makes the code messier. I'm open to switching to multiple writes though.

Copy link
Contributor

Choose a reason for hiding this comment

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

@mchlyu There is also xAsprintf if you need to allocate and print the format string in one call.

Copy link
Author

Choose a reason for hiding this comment

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

Ooh that might be useful, thanks for pointing that out!

Comment on lines +41 to +45
char* pid_str = String_cat(pid, "\t");
char* user = String_cat(this->user, "\t");
char* cmd = String_cat(Process_getCommand(this), "\n");
char* user_and_cmd = String_cat(user, cmd);
char* line = String_cat(pid_str, user_and_cmd);
Copy link
Member

Choose a reason for hiding this comment

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

Why not make this multiple writes into stdin of the new process instead? Apart from xSnprintf being the better tool for handling the %d\t%s\t part for PID+username here anyway.

Copy link
Author

Choose a reason for hiding this comment

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

What I said here, although I could be wrong or the performance difference doesn't matter that much or both.

Copy link
Member

Choose a reason for hiding this comment

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

The difference performance-wise is one vs. multiple syscalls, but given that malloc might syscall itself (mmap), there is not much of a difference in practice. Also I don't consider this a hot path, thus there's no pressing need for optimizations there.

Copy link
Author

Choose a reason for hiding this comment

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

Okay, thanks for clarifying; multiple writes will probably be much cleaner here.

Copy link
Contributor

@Explorer09 Explorer09 Dec 27, 2025

Choose a reason for hiding this comment

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

I wonder if we can use a separator other than tab.

The problem is that the cmdline can contain almost any character, including tab. I know it could be a pain to securely parse a cmdline string when it contains special characters, but we probably have to pass the cmdline string securely to the run script.

break;
}

if (res > 0) {
Copy link
Member

Choose a reason for hiding this comment

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

Invert condition and continue to reduce level of indentation. Cf. XReadFile API for other places that need this.

Copy link
Author

Choose a reason for hiding this comment

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

Got it, removed one level of indentation so far.
Regarding xReadFile, is that meant to be a function in xUtils? Because I can't find it in that file nor does searching online yield anything so I'm a bit confused.

Copy link
Member

Choose a reason for hiding this comment

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

Has been moved in the latest sources to Compat.c as readfd_internal with Compat_readfile and Compat_readfileat being official callers.

Copy link
Author

Choose a reason for hiding this comment

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

Got it, thanks for pointing out the new location.

@mchlyu
Copy link
Author

mchlyu commented Dec 27, 2025

@BenBE
I used Gemini to help me understand the codebase, explain portions of the feature request, help debug some segfaults, and suggest approaches. No code was copied from Gemini.
The last line of this PR and the gdb script were written by ChatGPT.
Should I update the PR to say co-authored by Gemini or GPT?

Thanks for pointing out a more robust approach. Regarding the command line of the process, could you please elaborate on how would I get the exact length of the command line, since right now I take whatever Process_getCommand gives me, and wherever that's terminated is the string I'd copy. If any program overwrote that string, I wouldn't know it's been changed in the first place right?

For merging the screens, that sounds like a good idea to me, although I'm less clear on which screen(s) this one is similar enough to be combined with. Perhaps something like CommandScreen?

@BenBE
Copy link
Member

BenBE commented Dec 31, 2025

For merging the existing screens, the most likely candidates are the strace screen, which IIRC is already based on the Command Screen implementation. Thus for running your own commands a similar base can be used.

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.

3 participants