From 832af1e906404a93db763a6d559929f3f3e276c0 Mon Sep 17 00:00:00 2001 From: mchlyu Date: Wed, 24 Dec 2025 00:30:38 -0800 Subject: [PATCH 1/2] Add ability to run arbitrary commands on keypress --- Action.c | 8 +++ Makefile.am | 4 ++ RunScript.c | 153 +++++++++++++++++++++++++++++++++++++++++++ RunScript.h | 21 ++++++ ScriptOutputScreen.c | 133 +++++++++++++++++++++++++++++++++++++ ScriptOutputScreen.h | 31 +++++++++ Settings.c | 7 ++ Settings.h | 1 + htop.1.in | 6 ++ 9 files changed, 364 insertions(+) create mode 100644 RunScript.c create mode 100644 RunScript.h create mode 100644 ScriptOutputScreen.c create mode 100644 ScriptOutputScreen.h diff --git a/Action.c b/Action.c index 1d3bccc51..e3d6deff8 100644 --- a/Action.c +++ b/Action.c @@ -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" @@ -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; @@ -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 } }; @@ -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; diff --git a/Makefile.am b/Makefile.am index 4492123f7..6a5151287 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 \ @@ -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 \ diff --git a/RunScript.c b/RunScript.c new file mode 100644 index 000000000..b2ac63317 --- /dev/null +++ b/RunScript.c @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#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); + char pid[pid_length + 1]; + snprintf(pid, pid_length + 1, "%d", row->id); + + 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); + + // 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); + // 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; + execl(htoprc_path, htoprc_path, 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); + } + + 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); +} diff --git a/RunScript.h b/RunScript.h new file mode 100644 index 000000000..e14c0499f --- /dev/null +++ b/RunScript.h @@ -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 diff --git a/ScriptOutputScreen.c b/ScriptOutputScreen.c new file mode 100644 index 000000000..b6be8ee20 --- /dev/null +++ b/ScriptOutputScreen.c @@ -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 +#include +#include +#include + +#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) { + int start = 0; + int num_tabs = 0; + 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++) { + 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 +}; diff --git a/ScriptOutputScreen.h b/ScriptOutputScreen.h new file mode 100644 index 000000000..6bc7bb18a --- /dev/null +++ b/ScriptOutputScreen.h @@ -0,0 +1,31 @@ +#ifndef HEADER_ScriptOutputScreen +#define HEADER_ScriptOutputScreen +/* +htop - ScriptOutputScreen.h +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "InfoScreen.h" +#include "Object.h" +#include "Process.h" +#include "RunScript.h" + + +typedef struct ScriptOutputScreen_ { + InfoScreen super; + int read_fd; + Node* data_head; + Node** data_tail; +} ScriptOutputScreen; + +extern const InfoScreenClass ScriptOutputScreen_class; + +ScriptOutputScreen* ScriptOutputScreen_new(const Process* process); + +void ScriptOutputScreen_delete(Object* this); + +void ScriptOutputScreen_SetFd(ScriptOutputScreen*, int); + +#endif diff --git a/Settings.c b/Settings.c index be0019788..ab64a9e74 100644 --- a/Settings.c +++ b/Settings.c @@ -53,6 +53,8 @@ void Settings_delete(Settings* this) { free(this->initialFilename); Settings_deleteColumns(this); Settings_deleteScreens(this); + if (this->scriptLocation) + free(this->scriptLocation); free(this); } @@ -552,6 +554,8 @@ static bool Settings_read(Settings* this, const char* fileName, const Machine* h free_and_xStrdup(&screen->dynamic, option[1]); Platform_addDynamicScreen(screen); } + } else if (String_eq(option[0], "script_location")) { + this->scriptLocation = xStrdup(option[1]); } String_freeArray(option); } @@ -736,6 +740,9 @@ int Settings_write(const Settings* this, bool onCrash) { printSettingInteger("tree_view_always_by_pid", this->screens[0]->treeViewAlwaysByPID); printSettingInteger("all_branches_collapsed", this->screens[0]->allBranchesCollapsed); + if (this->scriptLocation) + printSettingString("script_location", this->scriptLocation); + for (unsigned int i = 0; i < this->nScreens; i++) { ScreenSettings* ss = this->screens[i]; const char* sortKey = toFieldName(this->dynamicColumns, ss->sortKey, NULL); diff --git a/Settings.h b/Settings.h index 01e808e86..b693eae01 100644 --- a/Settings.h +++ b/Settings.h @@ -102,6 +102,7 @@ typedef struct Settings_ { bool headerMargin; bool screenTabs; bool showCachedMemory; + char* scriptLocation; #ifdef HAVE_GETMOUSE bool enableMouse; #endif diff --git a/htop.1.in b/htop.1.in index 8921dedab..eb26587e5 100644 --- a/htop.1.in +++ b/htop.1.in @@ -150,6 +150,12 @@ instead of the currently highlighted one. .B U Untag all processes (remove all tags added with the Space or c keys). .TP +.B r +Execute user script on tagged processes, or selected process if none tagged. +Script is passed each PID, user, and command in TSV format on stdin, and should +not be interactive. htop first searches for a file at $XDG_CONFIG_HOME/htop/run_script +before looking in htoprc for a full path associated with script_location. +.TP .B s Trace process system calls: if strace(1) is installed, pressing this key will attach it to the currently selected process, presenting a live From 408ba0863d6a9b8e2d170453c05faff00129b72f Mon Sep 17 00:00:00 2001 From: mchlyu Date: Sat, 24 Jan 2026 20:43:13 -0800 Subject: [PATCH 2/2] Change script data format to netstrings and add more secure exec method. --- RunScript.c | 118 +++++++++++++++++++++++++++++++++---------- RunScript.h | 4 ++ ScriptOutputScreen.c | 64 ++++++++++++----------- Settings.c | 5 +- 4 files changed, 129 insertions(+), 62 deletions(-) diff --git a/RunScript.c b/RunScript.c index b2ac63317..01d1db479 100644 --- a/RunScript.c +++ b/RunScript.c @@ -12,10 +12,12 @@ in the source distribution for its full text. #include #include #include +#include #include #include #include #include +#include #include #include "Action.h" @@ -34,33 +36,29 @@ 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); - char pid[pid_length + 1]; - snprintf(pid, pid_length + 1, "%d", row->id); + int pid_len = 0; + int pid = row->id; + while (pid > 0) { + pid /= 10; + pid_len++; + } - 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); + char* line; + 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)); - // 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)); + ssize_t res = write(write_fd, line_start, strlen(line_start)); 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); + free(line); } void RunScript(State* st) { @@ -95,26 +93,26 @@ void RunScript(State* st) { char* home = getenv("XDG_CONFIG_HOME"); if (!home) - home = String_cat(getenv("HOME"), "./config"); + 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); - // should not reach here unless execl fails + // executing script in root's directory, probably not malicious + root_exec(path, false); + // should not reach here unless fexecve fails fprintf(stderr, "error excuting %s\n", path); - perror("execl"); } else { // check if htoprc has something const char* htoprc_path = st->host->settings->scriptLocation; - execl(htoprc_path, htoprc_path, NULL); + // path can point to anything, so drop sudo for safety + root_exec(htoprc_path, true); - // only reach here if execl fails - fprintf(stderr, "error executing %s from htoprc", htoprc_path); + // only reach here if fexecve 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); + exit(127); } close(child_read[0]); @@ -151,3 +149,71 @@ void RunScript(State* st) { } ScriptOutputScreen_delete((Object*)sos); } + +void root_exec(const char* path, bool drop_sudo) { + // do not use O_CLOEXEC flag as that will cause fexecve to fail with ENOENT on a script + int fd = open(path, O_RDONLY); + if (fd < 0) { + perror("open"); + return; + } + // check that path is even a file + struct stat st; + if (fstat(fd, &st) == -1) { + perror("fstat"); + return; + } + + uid_t curr_uid = getuid(); + if (drop_sudo) { + // need to remove root from ourselves if we are root + if (curr_uid == 0) { + char* sudo_uid_str = getenv("SUDO_UID"); + if (!sudo_uid_str) { + fprintf(stderr, "sudo uid envar does not exist\n"); + return; + } + uid_t uid = strtoul(sudo_uid_str, NULL, 10); + if (uid == 0) { + fprintf(stderr, "sudo uid envar is root, failed to get uid of invoking user\n"); + return; + } + + char* sudo_gid_str = getenv("SUDO_GID"); + if (!sudo_gid_str) { + fprintf(stderr, "sudo gid envar does not exist\n"); + return; + } + gid_t gid = strtoul(sudo_gid_str, NULL, 10); + if (gid == 0) { + fprintf(stderr, "sudo gid envar is root group, failed to get gid of invoking user\n"); + return; + } + + // remove supplementary groups + if (setgroups(0, NULL) == -1) { + perror("setgroups"); + return; + } + if (setgid(gid) == -1) { + perror("setgid"); + return; + } + if (setuid(uid) == -1) { + perror("setuid"); + return; + } + } + } else if (curr_uid == 0 && (st.st_gid != 0 || st.st_uid != 0)) { + // we are root and script does not belongs to root, consider it unsafe + fprintf(stderr, "%s does not belong to root; has gid %u and uid %u\n", path, st.st_gid, st.st_uid); + return; + } + + static char* argv[] = {NULL, NULL}; + argv[0] = xStrdup(path); + static char* env[] = {NULL}; + fexecve(fd, argv, env); + + perror("fexecve"); +} diff --git a/RunScript.h b/RunScript.h index e14c0499f..fcc84f460 100644 --- a/RunScript.h +++ b/RunScript.h @@ -7,6 +7,8 @@ Released under the GNU GPLv2+, see the COPYING file in the source distribution for its full text. */ +#include + #include "Action.h" @@ -18,4 +20,6 @@ typedef struct Node_ { void RunScript(State*); +void root_exec(const char*, bool); + #endif diff --git a/ScriptOutputScreen.c b/ScriptOutputScreen.c index b6be8ee20..3bcf5b425 100644 --- a/ScriptOutputScreen.c +++ b/ScriptOutputScreen.c @@ -76,43 +76,41 @@ static void ScriptOutputScreen_scan(InfoScreen* super) { break; } - if (res > 0) { - int start = 0; - int num_tabs = 0; - 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++) { - if (buffer[j] == '\t') { - for (int k = 0; k < TABSIZE; k++) { - str[index++] = ' '; - } - } else { - str[index++] = buffer[j]; + size_t start = 0; + int num_tabs = 0; + for (size_t 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)); + size_t index = 0; + for (size_t j = start; j <= i; j++) { + 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; + } 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; } } } diff --git a/Settings.c b/Settings.c index ab64a9e74..d200e66c7 100644 --- a/Settings.c +++ b/Settings.c @@ -53,8 +53,7 @@ void Settings_delete(Settings* this) { free(this->initialFilename); Settings_deleteColumns(this); Settings_deleteScreens(this); - if (this->scriptLocation) - free(this->scriptLocation); + free(this->scriptLocation); free(this); } @@ -875,7 +874,7 @@ Settings* Settings_new(const Machine* host, Hashtable* dynamicMeters, Hashtable* #endif this->changed = false; this->delay = DEFAULT_DELAY; - + bool ok = Settings_read(this, this->filename, host, /*checkWritability*/true); if (!ok && legacyDotfile) { ok = Settings_read(this, legacyDotfile, host, this->writeConfig);