-
-
Notifications
You must be signed in to change notification settings - Fork 555
Add ability to run arbitrary commands on keypress #1843
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
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. So this pull request doesn't have either security mechanism yet. |
RunScript.c
Outdated
| const char* path = String_cat(home, "/htop/run_script"); | ||
| FILE* file = fopen(path, "r"); | ||
| if (file) { | ||
| execl(path, path, NULL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| execl(path, path, NULL); | |
| execl(path, path, (void*)NULL); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
RunScript.c
Outdated
| } else { | ||
| // check if htoprc has something | ||
| const char* htoprc_path = st->host->settings->scriptLocation; | ||
| execl(htoprc_path, htoprc_path, NULL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| execl(htoprc_path, htoprc_path, NULL); | |
| execl(htoprc_path, htoprc_path, (void*)NULL); |
BenBE
left a comment
There was a problem hiding this 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?
RunScript.c
Outdated
| char pid[pid_length + 1]; | ||
| snprintf(pid, pid_length + 1, "%d", row->id); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
RunScript.c
Outdated
| 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
ScriptOutputScreen.c
Outdated
| break; | ||
| } | ||
|
|
||
| if (res > 0) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
@BenBE 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 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 |
|
For merging the existing screens, the most likely candidates are the |
|
Hi, I've addressed the original feedback for the PR. Could you please take a look at it? For the dropping sudo permissions, I set it to only drop if reading a location from the htoprc since that could point to anything versus a run_script inside root's directory. Additionally, a program is only considered trusted if its owned by root. I'm unsure if this is a reasonable enough criteria so I'm open to suggestions to change it. I've also changed the passing of data to the script to use netstrings instead of TSV. Valgrind reports that some FDs that are still open, which I believe are the FDs used by fexecve. However, because setting O_CLOEXEC on a file that's a script causes fexecve to fail, I do not know if there is anyway to close these FDs after the exec call. Gemini helped explain #1844 for what dropping permissions would look like in Linux. |
| return; | ||
| } | ||
| } | ||
| } else if (curr_uid == 0 && (st.st_gid != 0 || st.st_uid != 0)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A program installed in /bin or /usr/bin may be owned by root but associated with a non-root group (e.g. "admin" or "wheel"). So I personally consider it too restrictive to require GID to be 0.
That's one issue I can think of. I would review other pieces of code if I have time.
| return; | ||
| } | ||
|
|
||
| uid_t curr_uid = getuid(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be geteuid, not getuid. The difference is not in the sudo use case, but when htop is launched with a SUID privilege.
| while (pid > 0) { | ||
| pid /= 10; | ||
| pid_len++; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have countDigits function in XUtils.h. Consider using it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies for missing that, thanks for pointing it out!
We can't assume the |
| int user_len = strlen(this->user); | ||
| int cmd_len = strlen(Process_getCommand(this)); | ||
| // writes pid_len:PID,user_len:User,cmd_len:Command\n in netstring format | ||
| xAsprintf(&line, "%d:%d,%d:%s,%d:%s\n", pid_len, row->id, user_len, this->user, cmd_len, Process_getCommand(this)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the netstring format? I don't think this makes passing the data safer while it unnecessarily complicates things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The netstring was meant to address this concern from @BenBE:
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 …
I understand that your original concern is the possibility of cmdline containing \t, and if netstrings don't improve safety, would it be acceptable to sanitize cmdline to remove any \t characters before passing it in the original TSV format?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mchlyu netstring is safe only if there's an agreement between sender and receiver on the length limit. You notice that the length field can contain any number of digits, so it's possible to craft a netstring that would cause arithmetic overflow on an unguarded receiver.
The security of the format comes with caveats, and I would say you didn't know it yet.
| perror("execl"); | ||
| } else { | ||
| // check if htoprc has something | ||
| const char* htoprc_path = st->host->settings->scriptLocation; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need clarification on what is the use case here.
Why do we have two ways to specify which command to execute?
I do have concern about this implementation: that the presence of one file can prevent the execution of another. It can also bring in an issue when the run_script file can be created or inserted by anyone other than the user running htop when an htop instance is being run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was from one of the feature requests in #506
Bonus points for making the script location configurable in
/etc/htoprcso that system-wide defaults could be set (having a$XDG_CONFIG_HOME/htop/run_scriptpresent would override the global setting still). This would (automatically) also allow to override the script location for a user in their localhtoprc(same config reader).
I might've misinterpreted this but I understood it as check for a run_script in $XDG_CONFIG_HOME, and if that doesn't exist, check htoprc.
This is also why I made the assumption earlier that when running htop as sudo, the run_script would need to be in directory owned by root, which could also be an incorrect assumption (my path resolves to /root/.config/htop/run_script when running with sudo).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was from one of the feature requests in #506
Bonus points for making the script location configurable in
/etc/htoprcso that system-wide defaults could be set (having a$XDG_CONFIG_HOME/htop/run_scriptpresent would override the global setting still). This would (automatically) also allow to override the script location for a user in their localhtoprc(same config reader).I might've misinterpreted this but I understood it as check for a
run_scriptin$XDG_CONFIG_HOME, and if that doesn't exist, checkhtoprc.
I would turn down on that part of the feature request because that doesn't help in the security at all.
Having a path in an htoprc file means htop has to memorize it during the settings' load procedure and write the same path when the setting file is saved.
The concrete use case of that extra path is unclear. And nothing can stop the user from making a symlink in the hard-coded location ($XDG_CONFIG_HOME/htop/run_script). So that custom path is redundant and should be turned down.
This is also why I made the assumption earlier that when running htop as sudo, the
run_scriptwould need to be in directory owned by root, which could also be an incorrect assumption (my path resolves to/root/.config/htop/run_scriptwhen running with sudo).
When htop is run with superuser privilege, the run_script should be run with superuser privilege only if it's owned by the same EUID, and has u+rx permission bits. Otherwise, if SUDO_USER is defined and the run_script is owned by that same user, run with that UID's privilege (setuid(2)). Similar principle applies when htop is run with SUID (i.e. geteuid() != getuid()).
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.
Created new InfoScreen pager heavily inspired by
OpenFilesScreenandTraceScreento 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.