Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
15 changes: 3 additions & 12 deletions CommandLine.c
Original file line number Diff line number Diff line change
Expand Up @@ -472,19 +472,10 @@ int CommandLine_run(int argc, char** argv) {
if (flags.commFilter)
setCommFilter(&state, &(flags.commFilter));

/* Set up shared search/filter history, stored next to the config file */
const char* rcPath = settings->filename;
const char* lastSlash = strrchr(rcPath, '/');
char historyPath[PATH_MAX];
if (lastSlash) {
int dirLen = (int)(lastSlash - rcPath + 1);
xSnprintf(historyPath, sizeof(historyPath), "%.*s" "htop_history", dirLen, rcPath);
} else {
/* no history file saved unless we have a sane rcPath */
historyPath[0] = '\0';
}

/* Set up shared search/filter history, stored below the XDG state directory */
char* historyPath = Settings_getHistoryFile(settings->filename);
IncSet_setHistoryFile(panel->inc, historyPath);
free(historyPath);

ScreenManager* scr = ScreenManager_new(header, host, &state, true);
ScreenManager_add(scr, (Panel*) panel, -1);
Expand Down
70 changes: 65 additions & 5 deletions History.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,66 @@ in the source distribution for its full text.

#include "History.h"

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>

#include "Macros.h"
#include "XUtils.h"


/* Determine whether the history file is safe to (over)write, mirroring the
checks Settings_read() applies to htoprc: the file must be a regular file
owned by the effective user with owner-write permission. The O_NOFOLLOW
flag guards the final path component against symlink attacks, while
O_NONBLOCK keeps an owned FIFO from blocking the read-only fallback. */
static void History_load(History* this) {
if (!this->filename)
return;
FILE* fp = fopen(this->filename, "r");
if (!fp)

int fd = -1;
do {
fd = open(this->filename, O_RDWR | O_NOCTTY | O_NOFOLLOW | O_NONBLOCK);
} while (fd < 0 && errno == EINTR);

if (fd < 0) {
this->writeHistory = (errno == ENOENT);
if (errno != EACCES && errno != EPERM && errno != EROFS)
return;
} else {
struct stat sb;
int err = fstat(fd, &sb);
this->writeHistory = !err && S_ISREG(sb.st_mode) && (sb.st_mode & S_IWUSR) && sb.st_uid == geteuid();
}

/* If opening read & write is not possible, open read only.
O_NOFOLLOW rejects a planted symlink, O_NONBLOCK avoids blocking on
non-regular files such as FIFOs when no writer is present. */
if (fd < 0) {
do {
fd = open(this->filename, O_RDONLY | O_NOCTTY | O_NOFOLLOW | O_NONBLOCK);
} while (fd < 0 && errno == EINTR);
}

if (fd < 0)
return;

/* Only read regular files; reading a FIFO would block. */
struct stat sb;
if (fstat(fd, &sb) != 0 || !S_ISREG(sb.st_mode)) {
close(fd);
return;
}

FILE* fp = fdopen(fd, "r");
if (!fp) {
close(fd);
return;
}

char line[LINEEDITOR_MAX + 2];
while (fgets(line, sizeof(line), fp)) {
Expand All @@ -48,6 +92,7 @@ History* History_new(const char* filename) {
this->position = 0;
this->saved[0] = '\0';
this->filename = filename ? xStrdup(filename) : NULL;
this->writeHistory = true;

if (this->filename)
History_load(this);
Expand All @@ -66,12 +111,27 @@ void History_delete(History* this) {
}

void History_save(const History* this) {
if (!this->filename)
if (!this->filename || !this->writeHistory)
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);
/* Settings_write writes things via a temp file & rename, we do it less robust but faster here.
O_NOFOLLOW guards against a symlink planted at the final path component,
O_NONBLOCK avoids hanging on an existing FIFO, and the fstat() re-check
closes a race between open and the owner verification. */
int fd = open(this->filename, O_WRONLY | O_NOCTTY | O_CREAT | O_NOFOLLOW | O_NONBLOCK, 0600);
if (fd == -1)
return;

struct stat sb;
if (fstat(fd, &sb) != 0 || !S_ISREG(sb.st_mode) || !(sb.st_mode & S_IWUSR) || sb.st_uid != geteuid()) {
close(fd);
return;
}

if (ftruncate(fd, 0) != 0) {
close(fd);
return;
}

FILE* fp = fdopen(fd, "w");
if (!fp) {
close(fd); // fd not consumed on failure, so close it
Expand Down
1 change: 1 addition & 0 deletions History.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ typedef struct History_ {
size_t position; /* current browse position: count = "at new input" */
char saved[LINEEDITOR_MAX + 1]; /* saved current input while browsing */
char* filename; /* path to history file (may be NULL = no read / write) */
bool writeHistory; /* whether the history file may be (over)written */
} History;

/* Create a new History, loading from the given file (may be NULL = init new history) */
Expand Down
130 changes: 125 additions & 5 deletions Settings.c
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,130 @@
return r;
}

static const char* Settings_getHome(void) {
const char* home = getenv("HOME");
if (!home || home[0] != '/') {
const struct passwd* pw = getpwuid(getuid());
return (pw && pw->pw_dir && pw->pw_dir[0] == '/') ? pw->pw_dir : "";
}
return home;
}

static bool Settings_mkdirp(const char* path, mode_t mode) {
char* copy = xStrdup(path);
bool ok = true;
for (char* p = copy + (copy[0] == '/' ? 1 : 0); *p; p++) {
if (*p != '/')
continue;
*p = '\0';
if (mkdir(copy, mode) != 0 && errno != EEXIST)
ok = false;
*p = '/';
}
if (mkdir(copy, mode) != 0 && errno != EEXIST)
ok = false;
free(copy);
return ok;
}

static void Settings_migrateHistory(const char* fromPath, const char* toPath) {
/* O_NOFOLLOW and O_NONBLOCK ensure a symlink or FIFO planted at the path
cannot redirect the copy or block it. */
int fromFd = open(fromPath, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
if (fromFd < 0)
return;

/* Validate the descriptor we actually opened: regular file owned by the
effective user. O_NOFOLLOW rejects a symlink at the final path component,
and fstat() rules out a swap between open() and here. */
struct stat sb;
if (fstat(fromFd, &sb) != 0 || !S_ISREG(sb.st_mode) || sb.st_uid != geteuid()) {
close(fromFd);
return;
}

/* O_EXCL guarantees the destination is never overwritten; once the state
file exists it takes precedence over the legacy copy. */
int toFd = open(toPath, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_NONBLOCK, 0600);
if (toFd < 0) {
close(fromFd);
return;
}

bool ok = true;
char buf[4096];
for (;;) {
ssize_t n = read(fromFd, buf, sizeof(buf));
if (n < 0) {
if (errno == EINTR)
continue;
ok = false;
break;
}
if (n == 0)
break;
if (full_write(toFd, buf, (size_t)n) != n) {
ok = false;
break;
}
}
if (close(fromFd) != 0)
ok = false;
if (close(toFd) != 0 || !ok) {
unlink(toPath);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
return;
}

/* Remove the legacy file only if it still refers to the entry we copied. */
struct stat sbPath;
if (lstat(fromPath, &sbPath) == 0 && sbPath.st_dev == sb.st_dev && sbPath.st_ino == sb.st_ino)
(void) unlink(fromPath);
Comment thread
fasterit marked this conversation as resolved.
Dismissed
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/* Return the legacy history path as stored beside a configuration file
(the old history location), or NULL when the file has no directory part. */
static char* Settings_legacyHistoryFile(const char* configFile) {
const char* lastSlash = strrchr(configFile, '/');
if (!lastSlash)
return NULL;
char* dir = xStrndup(configFile, (size_t)(lastSlash - configFile + 1));
char* file = String_cat(dir, "htop_history");
free(dir);
return file;
}

char* Settings_getHistoryFile(const char* configFile) {
const char* xdgStateHome = getenv("XDG_STATE_HOME");
const char* home = Settings_getHome();

if ((!xdgStateHome || xdgStateHome[0] != '/') && !home[0])
return NULL;

char* stateHtopDir;
if (xdgStateHome && xdgStateHome[0] == '/')
stateHtopDir = String_cat(xdgStateHome, "/htop");
else
stateHtopDir = String_cat(home, "/.local/state/htop");

char* historyFile = String_cat(stateHtopDir, "/htop_history");
if (!Settings_mkdirp(stateHtopDir, 0700)) {
free(stateHtopDir);
free(historyFile);
return NULL;
}
free(stateHtopDir);

/* The search/filter history used to be stored next to the
htoprc file; migrate it if present. */
char* legacyFile = Settings_legacyHistoryFile(configFile);
if (legacyFile) {
Settings_migrateHistory(legacyFile, historyFile);
free(legacyFile);
}

return historyFile;
}

Settings* Settings_new(const Machine* host, Hashtable* dynamicMeters, Hashtable* dynamicColumns, Hashtable* dynamicScreens) {
Settings* this = xCalloc(1, sizeof(Settings));

Expand Down Expand Up @@ -963,11 +1087,7 @@
if (rcfile) {
this->initialFilename = xStrdup(rcfile);
} else {
const char* home = getenv("HOME");
if (!home || home[0] != '/') {
const struct passwd* pw = getpwuid(getuid());
home = (pw && pw->pw_dir && pw->pw_dir[0] == '/') ? pw->pw_dir : "";
}
const char* home = Settings_getHome();
const char* xdgConfigHome = getenv("XDG_CONFIG_HOME");
char* configDir = NULL;
char* htopDir = NULL;
Expand Down
6 changes: 6 additions & 0 deletions Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,10 @@ bool Settings_isReadonly(void);

void Settings_setHeaderLayout(Settings* this, HeaderLayout hLayout);

/* Return the path of the search/filter history file, located below the XDG
state directory. Ensures the directory exists and migrates a legacy history
file stored next to the active configuration file. Returns NULL if no
suitable home directory can be determined. */
char* Settings_getHistoryFile(const char* configFile);

#endif
11 changes: 11 additions & 0 deletions htop.1.in
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,17 @@ tries to read the system-wide configuration from
.I @sysconfdir@/htoprc
and as a last resort, falls back to its hard coded defaults.
.LP
The search and filter history is stored below the XDG state directory, in
.IR $XDG_STATE_HOME/htop/htop_history ,
defaulting to
.IR ~/.local/state/htop/htop_history
when the
.IR $XDG_STATE_HOME
variable is not set.
A history file left over from an older version stored next to the htoprc
configuration file is migrated to this location automatically on the next
start.
.LP
You may override the location of the configuration file using the $HTOPRC
environment variable (so you can have multiple configurations for different
machines that share the same home directory, for example).
Expand Down
Loading