Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Action.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ in the source distribution for its full text.
#include "ProvideCurses.h"
#include "Row.h"
#include "RowField.h"
#include "RunScript.h"
#include "Scheduling.h"
#include "ScreenManager.h"
#include "SignalsPanel.h"
Expand Down Expand Up @@ -646,6 +647,11 @@ static Htop_Reaction actionTogglePauseUpdate(State* st) {
return HTOP_REFRESH | HTOP_REDRAW_BAR | HTOP_KEEP_FOLLOWING;
}

static Htop_Reaction actionRunScript(State* st) {
RunScript(st);
return HTOP_OK;
}

static const struct {
const char* key;
bool roInactive;
Expand Down Expand Up @@ -700,6 +706,7 @@ static const struct {
{ .key = " F2 C S: ", .roInactive = false, .info = "setup" },
{ .key = " F1 h ?: ", .roInactive = false, .info = "show this help screen" },
{ .key = " F10 q: ", .roInactive = false, .info = "quit" },
{ .key = " r: ", .roInactive = false, .info = "execute user script on tagged processes"},
{ .key = NULL, .info = NULL }
};

Expand Down Expand Up @@ -939,6 +946,7 @@ void Action_setBindings(Htop_Action* keys) {
keys['m'] = actionToggleMergedCommand;
keys['p'] = actionToggleProgramPath;
keys['q'] = actionQuit;
keys['r'] = actionRunScript;
keys['s'] = actionStrace;
keys['t'] = actionToggleTreeView;
keys['u'] = actionFilterByUser;
Expand Down
4 changes: 4 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,13 @@ myhtopsources = \
ProcessLocksScreen.c \
ProcessTable.c \
Row.c \
RunScript.c \
RichString.c \
Scheduling.c \
ScreenManager.c \
ScreensPanel.c \
ScreenTabsPanel.c \
ScriptOutputScreen.c \
Settings.c \
SignalsPanel.c \
SwapMeter.c \
Expand Down Expand Up @@ -148,11 +150,13 @@ myhtopheaders = \
ProvideTerm.h \
RichString.h \
Row.h \
RunScript.h \
RowField.h \
Scheduling.h \
ScreenManager.h \
ScreensPanel.h \
ScreenTabsPanel.h \
ScriptOutputScreen.h \
Settings.h \
SignalsPanel.h \
SwapMeter.h \
Expand Down
153 changes: 153 additions & 0 deletions RunScript.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
htop - RunScript.h
(C) 2025 htop dev team
Released under the GNU GPLv2+, see the COPYING file
in the source distribution for its full text.
*/

#include "config.h" // IWYU pragma: keep

#include "RunScript.h"

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "Action.h"
#include "InfoScreen.h"
#include "MainPanel.h"
#include "Object.h"
#include "Panel.h"
#include "Process.h"
#include "Row.h"
#include "ScriptOutputScreen.h"
#include "Settings.h"
#include "XUtils.h"


static void write_row(Row* row, int write_fd) {
Process* this = (Process*) row;
assert(Object_isA((const Object*) this, (const ObjectClass*) &Process_class));

int pid_length = snprintf(NULL, 0, "%d", row->id);
Comment thread
mchlyu marked this conversation as resolved.
Outdated
char pid[pid_length + 1];
snprintf(pid, pid_length + 1, "%d", row->id);

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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!


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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

@Explorer09 Explorer09 Dec 27, 2025

Copy link
Copy Markdown
Contributor

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.


// writes PID\tUser\tCommand\n, using TSV format
size_t count = strlen(line);
char* line_start = line;
while (count > 0) {
ssize_t res = write(write_fd, line_start, strlen(line));
if ((res == -1 && errno != EINTR) || res == 0)
break;

count -= res;
line_start += res;
}

free(pid_str);
free(user);
free(cmd);
free(user_and_cmd);
free(line);
}

void RunScript(State* st) {
int child_read[2] = {0, 0};
int child_write[2] = {0, 0};

if (pipe(child_read) == -1)
return;
if (pipe(child_write) == -1) {
close(child_read[0]);
close(child_read[1]);
return;
}

pid_t child = fork();
if (child == -1) {
close(child_read[0]);
close(child_read[1]);
close(child_write[0]);
close(child_write[1]);
fprintf(stderr, "fork failed\n");
return;
} else if (child == 0) {
close(child_read[1]);
dup2(child_read[0], STDIN_FILENO);
close(child_read[0]);

close(child_write[0]);
dup2(child_write[1], STDOUT_FILENO);
dup2(child_write[1], STDERR_FILENO);
close(child_write[1]);

char* home = getenv("XDG_CONFIG_HOME");
if (!home)
home = String_cat(getenv("HOME"), "./config");

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

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

// should not reach here unless execl fails
fprintf(stderr, "error excuting %s\n", path);
perror("execl");
} else {
// check if htoprc has something
const char* htoprc_path = st->host->settings->scriptLocation;

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Author

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/htoprc so that system-wide defaults could be set (having a $XDG_CONFIG_HOME/htop/run_script present would override the global setting still). This would (automatically) also allow to override the script location for a user in their local htoprc (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).

Copy link
Copy Markdown
Contributor

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/htoprc so that system-wide defaults could be set (having a $XDG_CONFIG_HOME/htop/run_script present would override the global setting still). This would (automatically) also allow to override the script location for a user in their local htoprc (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.

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_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).

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()).

execl(htoprc_path, htoprc_path, NULL);

Copy link
Copy Markdown
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);


// only reach here if execl fails
fprintf(stderr, "error executing %s from htoprc", htoprc_path);
fprintf(stderr, "if you expected your runscript to be executed, htop looked for it at %s", path);
perror("execl");
}
exit(1);
Comment thread
mchlyu marked this conversation as resolved.
Outdated
}

close(child_read[0]);
close(child_write[1]);

bool anyTagged = false;
Panel* super = &st->mainPanel->super;
for (int i = 0; i < Panel_size(super); i++) {
Row* row = (Row*) Panel_get(super, i);
if (row->tag) {
write_row(row, child_read[1]);
anyTagged = true;
}
}
// if nothing was tagged, operate on the highlighted row
if (!anyTagged) {
Row* row = (Row*) Panel_getSelected(super);
if (row)
write_row(row, child_read[1]);
}

// tell script/child we're done with sending input
close(child_read[1]);

const Process* p = (Process*) Panel_getSelected((Panel*)st->mainPanel);
if (!p)
return;

assert(Object_isA((const Object*) p, (const ObjectClass*) &Process_class));
ScriptOutputScreen* sos = ScriptOutputScreen_new(p);
if (fcntl(child_write[0], F_SETFL, O_NONBLOCK) >= 0) {
ScriptOutputScreen_SetFd(sos, child_write[0]);
InfoScreen_run((InfoScreen*)sos);
}
ScriptOutputScreen_delete((Object*)sos);
}
21 changes: 21 additions & 0 deletions RunScript.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#ifndef RUNSCRIPT_Process
#define RUNSCRIPT_Process
/*
htop - RunScript.h
(C) 2025 htop dev team
Released under the GNU GPLv2+, see the COPYING file
in the source distribution for its full text.
*/

#include "Action.h"


typedef struct Node_ {
char* line;
struct Node_* next;
} Node;


void RunScript(State*);

#endif
133 changes: 133 additions & 0 deletions ScriptOutputScreen.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
htop - ScriptOutputScreen.c
(C) 2025 htop dev team
Released under the GNU GPLv2+, see the COPYING file
in the source distribution for its full text.
*/

#include "config.h" // IWYU pragma: keep

#include "ScriptOutputScreen.h"

#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

#include "Panel.h"
#include "ProvideCurses.h"
#include "XUtils.h"


ScriptOutputScreen* ScriptOutputScreen_new(const Process* process) {
ScriptOutputScreen* this = xCalloc(1, sizeof(ScriptOutputScreen));
Object_setClass(this, Class(ScriptOutputScreen));
// this fd needs to be set later
this->read_fd = -1;
this->data_head = NULL;
this->data_tail = &this->data_head;
return (ScriptOutputScreen*) InfoScreen_init(&this->super, process, NULL, LINES - 2, " ");
}

void ScriptOutputScreen_SetFd(ScriptOutputScreen* this, int fd) {
this->read_fd = fd;
}

void ScriptOutputScreen_delete(Object* this) {
// free the linked list and close fd
assert(Object_isA((const Object*) this, (const ObjectClass*) &ScriptOutputScreen_class));
Node* walk = ((ScriptOutputScreen*)this)->data_head;
while (walk) {
free(walk->line);
Node* next = walk->next;
free(walk);
walk = next;
}
close(((ScriptOutputScreen*)this)->read_fd);
free(InfoScreen_done((InfoScreen*)this));
}

static void ScriptOutputScreen_scan(InfoScreen* super) {
Panel* panel = super->display;
int idx = Panel_getSelectedIndex(panel);
Panel_prune(panel);

char buffer[8192];
ScriptOutputScreen* sos = ((ScriptOutputScreen*)super);
assert(Object_isA((const Object*) sos, (const ObjectClass*) &ScriptOutputScreen_class));

// redraw existing stuff in the screen first
Node* walk = sos->data_head;
while (walk) {
InfoScreen_addLine(super, walk->line);
walk = walk->next;
}

for (;;) {
ssize_t res = read(sos->read_fd, buffer, sizeof(buffer) - 1);
if (res < 0) {
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
continue;

break;
}

if (res == 0) {
break;
}

if (res > 0) {

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

int start = 0;
int num_tabs = 0;
Comment thread
mchlyu marked this conversation as resolved.
Outdated
for (int i = 0; i <= res; i++) {
num_tabs += (buffer[i] == '\t');
// split line when find \n or exhaust buffer
if (i == res || buffer[i] == '\n') {
buffer[i] = '\0';
char* str;
if (num_tabs > 0) {
// manually replace all \t with TABSIZE spaces
str = xMalloc((num_tabs * TABSIZE + i - start + 1) * sizeof(char));
int index = 0;
for (int j = start; j <= i; j++) {
Comment thread
mchlyu marked this conversation as resolved.
Outdated
if (buffer[j] == '\t') {
for (int k = 0; k < TABSIZE; k++) {
str[index++] = ' ';
}
} else {
str[index++] = buffer[j];
}
}
} else {
str = buffer + start;
}
InfoScreen_addLine(super, str);
// store line for next redraw
*(sos->data_tail) = xMalloc(sizeof(Node));
(*sos->data_tail)->line = xStrdup(str);
(*sos->data_tail)->next = NULL;
*(&sos->data_tail) = &((*sos->data_tail)->next);

if (num_tabs > 0)
free(str);
start = i + 1;
num_tabs = 0;
}
}
}
}
Panel_setSelected(panel, idx);
}

static void ScriptOutputScreen_draw(InfoScreen* this ) {
InfoScreen_drawTitled(this, "Output of script for process %d - %s", Process_getPid(this->process), Process_getCommand(this->process));
}

const InfoScreenClass ScriptOutputScreen_class = {
.super = {
.extends = Class(Object),
.delete = ScriptOutputScreen_delete
},
.scan = ScriptOutputScreen_scan,
.draw = ScriptOutputScreen_draw
};
Loading