From d76c049cc4b1da8d5b42f0013ecdcb4d592c9282 Mon Sep 17 00:00:00 2001 From: Denis Vlasenko Date: Fri, 25 May 2007 02:16:25 +0000 Subject: [PATCH] hush: rework variable storage and environment handling. More that -100 bytes of code + memory leak plugged. Added a testcase for it. --- shell/README | 4 + shell/hush.c | 328 ++++++++++----------- shell/hush_test/hush-z_slow/leak_var.right | 2 + shell/hush_test/hush-z_slow/leak_var.tests | 69 +++++ 4 files changed, 237 insertions(+), 166 deletions(-) create mode 100644 shell/hush_test/hush-z_slow/leak_var.right create mode 100755 shell/hush_test/hush-z_slow/leak_var.tests diff --git a/shell/README b/shell/README index a9b434637..a09353d6c 100644 --- a/shell/README +++ b/shell/README @@ -1,5 +1,9 @@ Various bits of what is known about busybox shells, in no particular order. +2007-05-24 +hush: environment-related memory leak plugged, with net code size +decrease. + 2007-05-24 hush: '( echo ${name )' will show syntax error message, but prompt doesn't return (need to press ). Pressing Ctrl-C, , diff --git a/shell/hush.c b/shell/hush.c index d96615d54..0b8e7d68d 100644 --- a/shell/hush.c +++ b/shell/hush.c @@ -37,7 +37,6 @@ * across continuation lines. * * Bash grammar not implemented: (how many of these were in original sh?) - * $@ (those sure look like weird quoting rules) * $_ * ! negation operator for pipes * &> and >& redirection of stdout+stderr @@ -279,11 +278,18 @@ struct close_me { int fd; }; -struct variables { - struct variables *next; - const char *name; - const char *value; - smallint flg_export; +/* On program start, environ points to initial environment. + * putenv adds new pointers into it, unsetenv removes them. + * Neither of these (de)allocates the strings. + * setenv allocates new strings in malloc space and does putenv, + * and thus setenv is unusable (leaky) for shell's purposes */ +#define setenv(...) setenv_is_leaky_dont_use() +struct variable { + struct variable *next; + char *name; /* points to "name=" portion */ + char *value; /* points directly after "=" */ + int max_len; /* if > 0, name is part of initial env; else name is malloced */ + smallint flg_export; /* putenv should be done on this var */ smallint flg_read_only; }; @@ -322,14 +328,21 @@ enum { CHAR_SPECIAL = 3, /* example: $ */ }; +#define HUSH_VER_STR "0.02" + +static const char version_str[] = "HUSH_VERSION="HUSH_VER_STR; + +static const struct variable const_shell_ver = { + .next = NULL, + .name = (char*)version_str, + .value = (char*)version_str + sizeof("HUSH_VERSION=")-1, + .max_len = 1, /* 0 can provoke free(name) */ + .flg_export = 1, + .flg_read_only = 1, +}; /* "Globals" within this file */ -#define HUSH_VER_STR "0.02" -static const struct variables const_shell_ver = { - NULL, "HUSH_VERSION", HUSH_VER_STR, 1, 1 -}; - /* Sorted roughly by size (smaller offsets == smaller code) */ struct globals { #if ENABLE_HUSH_INTERACTIVE @@ -360,8 +373,8 @@ struct globals { struct close_me *close_me_head; const char *cwd; unsigned last_bg_pid; - struct variables *top_vars; /* = &shell_ver (both are set in main()) */ - struct variables shell_ver; /* = const_shell_ver */ + struct variable *top_var; /* = &shell_ver (both are set in main()) */ + struct variable shell_ver; /* = const_shell_ver */ #if ENABLE_FEATURE_SH_STANDALONE struct nofork_save_area nofork_save; #endif @@ -407,7 +420,7 @@ enum { run_list_level = 0 }; #define close_me_head (G.close_me_head ) #define cwd (G.cwd ) #define last_bg_pid (G.last_bg_pid ) -#define top_vars (G.top_vars ) +#define top_var (G.top_var ) #define shell_ver (G.shell_ver ) #if ENABLE_FEATURE_SH_STANDALONE #define nofork_save (G.nofork_save ) @@ -541,8 +554,8 @@ static char **expand_strvec_to_strvec(char **argv); static char *expand_strvec_to_string(char **argv); /* used for expansion of right hand of assignments */ static char *expand_string_to_string(const char *str); -static const char *get_local_var(const char *var); -static int set_local_var(const char *s, int flg_export); +static struct variable *get_local_var(const char *var); +static int set_local_var(char *s, int flg_export); static void unset_local_var(const char *name); /* Table of built-in functions. They can be forked or not, depending on @@ -795,7 +808,7 @@ static int builtin_exit(char **argv) /* built-in 'export VAR=value' handler */ static int builtin_export(char **argv) { - int res = 0; + const char *value; char *name = argv[1]; if (name == NULL) { @@ -810,36 +823,23 @@ static int builtin_export(char **argv) return EXIT_SUCCESS; } - name = xstrdup(name); - { - const char *value = strchr(name, '='); + value = strchr(name, '='); + if (!value) { + /* They are exporting something without a =VALUE */ + struct variable *var; - if (!value) { - char *tmp; - /* They are exporting something without an =VALUE */ - - value = get_local_var(name); - if (value) { - size_t ln = strlen(name); - - tmp = xrealloc(name, ln+strlen(value)+2); - sprintf(tmp+ln, "=%s", value); - name = tmp; - } else { - /* bash does not return an error when trying to export - * an undefined variable. Do likewise. */ - res = 1; - } + var = get_local_var(name); + if (var) { + var->flg_export = 1; + putenv(var->name); } + /* bash does not return an error when trying to export + * an undefined variable. Do likewise. */ + return EXIT_SUCCESS; } - if (res < 0) - bb_perror_msg("export"); - else if (res == 0) - res = set_local_var(name, 1); - else - res = 0; - free(name); - return res; + + set_local_var(xstrdup(name), 1); + return EXIT_SUCCESS; } #if ENABLE_HUSH_JOB @@ -965,20 +965,20 @@ static int builtin_read(char **argv) /* read string. name_len+1 chars are already used by 'name=' */ fgets(p, sizeof(string) - 1 - name_len, stdin); chomp(p); - return set_local_var(string, 0); + return set_local_var(xstrdup(string), 0); } /* built-in 'set [VAR=value]' handler */ static int builtin_set(char **argv) { char *temp = argv[1]; - struct variables *e; + struct variable *e; if (temp == NULL) - for (e = top_vars; e; e = e->next) - printf("%s=%s\n", e->name, e->value); + for (e = top_var; e; e = e->next) + puts(e->name); else - set_local_var(temp, 0); + set_local_var(xstrdup(temp), 0); return EXIT_SUCCESS; } @@ -1742,26 +1742,9 @@ static int run_pipe_real(struct pipe *pi) if (i != 0 && argv[i] == NULL) { /* assignments, but no command: set the local environment */ for (i = 0; argv[i] != NULL; i++) { - /* Ok, this case is tricky. We have to decide if this is a - * local variable, or an already exported variable. If it is - * already exported, we have to export the new value. If it is - * not exported, we need only set this as a local variable. - * This junk is all to decide whether or not to export this - * variable. */ - int export_me = 0; - char *name, *value; - name = xstrdup(argv[i]); - debug_printf("local environment set: %s\n", name); - value = strchr(name, '='); - if (value) - *value = '\0'; - if (get_local_var(name)) { - export_me = 1; - } - free(name); + debug_printf("local environment set: %s\n", argv[i]); p = expand_string_to_string(argv[i]); - set_local_var(p, export_me); - free(p); + set_local_var(p, 0); } return EXIT_SUCCESS; /* don't worry about errors in set_local_var() yet */ } @@ -2693,109 +2676,115 @@ static char* expand_strvec_to_string(char **argv) } /* This is used to get/check local shell variables */ -static const char *get_local_var(const char *s) +static struct variable *get_local_var(const char *s) { - struct variables *cur; + struct variable *cur; + int len; if (!s) return NULL; - for (cur = top_vars; cur; cur = cur->next) { - if (strcmp(cur->name, s) == 0) - return cur->value; + len = strlen(s); + for (cur = top_var; cur; cur = cur->next) { + if (strncmp(cur->name, s, len) == 0 && cur->name[len] == '=') + return cur; } return NULL; } -/* This is used to set local shell variables - flg_export == 0 if only local (not exporting) variable - flg_export == 1 if "new" exporting environ - flg_export > 1 if current startup environ (not call putenv()) */ -static int set_local_var(const char *s, int flg_export) +/* name holds "NAME=VAL" and is expected to be malloced. + * We take ownership of it. */ +static int set_local_var(char *name, int flg_export) { - char *name, *value; - int result = 0; - struct variables *cur; + struct variable *cur; + char *value; + int name_len; - name = xstrdup(s); - - /* Assume when we enter this function that we are already in - * NAME=VALUE format. So the first order of business is to - * split 's' on the '=' into 'name' and 'value' */ value = strchr(name, '='); - /*if (value == 0 && ++value == 0) ??? -vda */ - if (value == NULL || value[1] == '\0') { + if (!value) { /* not expected to ever happen? */ free(name); return -1; } - *value++ = '\0'; - for (cur = top_vars; cur; cur = cur->next) { - if (strcmp(cur->name, name) == 0) { - if (strcmp(cur->value, value) == 0) { - if (flg_export && !cur->flg_export) - cur->flg_export = flg_export; - else - result++; - } else if (cur->flg_read_only) { - bb_error_msg("%s: readonly variable", name); - result = -1; - } else { - if (flg_export > 0 || cur->flg_export > 1) - cur->flg_export = 1; - free((char*)cur->value); - cur->value = xstrdup(value); + name_len = value - name; + cur = top_var; /* cannot be NULL (we have HUSH_VERSION and it's RO) */ + while (1) { + if (strncmp(cur->name, name, name_len) != 0 || cur->name[name_len] != '=') { + if (!cur->next) { + /* cur points to last var in linked list */ + break; } - goto skip; + cur = cur->next; + continue; } + /* We already have a var with this name */ + if (cur->flg_read_only) { + bb_error_msg("%s: readonly variable", name); + free(name); + return -1; + } + *value = '\0'; + unsetenv(name); /* just in case */ + *value++ = '='; + if (strcmp(cur->value, value) == 0) { + free_and_exp: + free(name); + goto exp; + } + if (cur->max_len >= strlen(name)) { + /* This one is from startup env, reuse space */ + strcpy(cur->name, name); + goto free_and_exp; + } + /* max_len == 0 signifies "malloced" var, which we can + * (and has to) free */ + if (!cur->max_len) + free(cur->name); + cur->max_len = 0; + goto set_name_and_exp; } - cur = xzalloc(sizeof(*cur)); - /*cur->next = 0;*/ - cur->name = xstrdup(name); - cur->value = xstrdup(value); - cur->flg_export = flg_export; - /*cur->flg_read_only = 0;*/ - { - struct variables *bottom = top_vars; - while (bottom->next) - bottom = bottom->next; - bottom->next = cur; - } - skip: - if (result == 0 && cur->flg_export == 1) { - *(value-1) = '='; - result = putenv(name); - } else { - free(name); - if (result > 0) /* equivalent to previous set */ - result = 0; - } - return result; + /* Not found - create next variable struct */ + cur->next = xzalloc(sizeof(*cur)); + cur = cur->next; + + set_name_and_exp: + cur->name = name; + exp: + cur->value = cur->name + name_len + 1; + if (flg_export) + cur->flg_export = 1; + if (cur->flg_export) + return putenv(cur->name); + return 0; } static void unset_local_var(const char *name) { - struct variables *cur, *next; + struct variable *cur; + struct variable *prev = prev; /* for gcc */ + int name_len; if (!name) return; - for (cur = top_vars; cur; cur = cur->next) { - if (strcmp(cur->name, name) == 0) { + name_len = strlen(name); + cur = top_var; + while (cur) { + if (strncmp(cur->name, name, name_len) == 0 && cur->name[name_len] == '=') { if (cur->flg_read_only) { bb_error_msg("%s: readonly variable", name); return; } - if (cur->flg_export) - unsetenv(cur->name); - free((char*)cur->name); - free((char*)cur->value); - next = top_vars; - while (next->next != cur) - next = next->next; - next->next = cur->next; + /* prev is ok to use here because 1st variable, HUSH_VERSION, + * is ro, and we cannot reach this code on the 1st pass */ + prev->next = cur->next; + unsetenv(cur->name); + if (!cur->max_len) + free(cur->name); free(cur); return; } + prev = cur; + cur = cur->next; } } @@ -3265,13 +3254,10 @@ static int parse_group(o_string *dest, struct p_context *ctx, * see the bash man page under "Parameter Expansion" */ static const char *lookup_param(const char *src) { - const char *p = NULL; - if (src) { - p = getenv(src); - if (!p) - p = get_local_var(src); - } - return p; + struct variable *var = get_local_var(src); + if (var) + return var->value; + return NULL; } /* return code: 0 for OK, 1 for syntax error */ @@ -3681,10 +3667,29 @@ int hush_main(int argc, char **argv) int opt; FILE *input; char **e; + struct variable *cur_var; PTR_TO_GLOBALS = xzalloc(sizeof(G)); - top_vars = &shell_ver; + shell_ver = const_shell_ver; /* copying struct here */ + top_var = &shell_ver; + /* initialize our shell local variables with the values + * currently living in the environment */ + e = environ; + cur_var = top_var; + if (e) while (*e) { + char *value = strchr(*e, '='); + if (value) { /* paranoia */ + cur_var->next = xzalloc(sizeof(*cur_var)); + cur_var = cur_var->next; + cur_var->name = *e; + cur_var->value = value + 1; + cur_var->max_len = strlen(*e); + cur_var->flg_export = 1; + } + e++; + } + putenv(shell_ver.name); #if ENABLE_FEATURE_EDITING line_input_state = new_line_input_t(FOR_SHELL); @@ -3701,14 +3706,8 @@ int hush_main(int argc, char **argv) PS2 = "> "; #endif - /* initialize our shell local variables with the values - * currently living in the environment */ - e = environ; - if (e) - while (*e) - set_local_var(*e++, 2); /* without call putenv() */ - - last_return_code = EXIT_SUCCESS; + if (EXIT_SUCCESS) /* otherwise is already done */ + last_return_code = EXIT_SUCCESS; if (argv[0] && argv[0][0] == '-') { debug_printf("sourcing /etc/profile\n"); @@ -3818,23 +3817,20 @@ int hush_main(int argc, char **argv) input = xfopen(argv[optind], "r"); opt = parse_and_run_file(input); + final_return: + #if ENABLE_FEATURE_CLEAN_UP fclose(input); if (cwd != bb_msg_unknown) free((char*)cwd); - { - struct variables *cur, *tmp; - for (cur = top_vars; cur; cur = tmp) { - tmp = cur->next; - if (!cur->flg_read_only) { - free((char*)cur->name); - free((char*)cur->value); - free(cur); - } - } + cur_var = top_var->next; + while (cur_var) { + struct variable *tmp = cur_var; + if (!cur_var->max_len) + free(cur_var->name); + cur_var = cur_var->next; + free(tmp); } #endif - - final_return: hush_exit(opt ? opt : last_return_code); } diff --git a/shell/hush_test/hush-z_slow/leak_var.right b/shell/hush_test/hush-z_slow/leak_var.right new file mode 100644 index 000000000..7bccc1eef --- /dev/null +++ b/shell/hush_test/hush-z_slow/leak_var.right @@ -0,0 +1,2 @@ +Measuring memory leak... +vsz does not grow diff --git a/shell/hush_test/hush-z_slow/leak_var.tests b/shell/hush_test/hush-z_slow/leak_var.tests new file mode 100755 index 000000000..d3ca25908 --- /dev/null +++ b/shell/hush_test/hush-z_slow/leak_var.tests @@ -0,0 +1,69 @@ +pid=$$ + +# Warm up +unset t +t=111111111111111111111111111111111111111111111111111111111111111111111111 +export t +unset t +t=111111111111111111111111111111111111111111111111111111111111111111111111 +export t +unset t +t=111111111111111111111111111111111111111111111111111111111111111111111111 +export t +unset t +t=111111111111111111111111111111111111111111111111111111111111111111111111 +export t +unset t +t=111111111111111111111111111111111111111111111111111111111111111111111111 +export t +i=1 +if test $i = 1111111111111111111111111111111111111111111111; then i=2; fi +beg=`ps -o pid,vsz | grep "^ *$pid "` + +echo "Measuring memory leak..." +beg=`ps -o pid,vsz | grep "^ *$pid "` +i=1 +while test $i != X; do + unset t + t=111111111111111111111111111111111111111111111111111111111111111111111111 + export t + unset t + t=111111111111111111111111111111111111111111111111111111111111111111111111 + export t + unset t + t=111111111111111111111111111111111111111111111111111111111111111111111111 + export t + unset t + t=111111111111111111111111111111111111111111111111111111111111111111111111 + export t + unset t + t=111111111111111111111111111111111111111111111111111111111111111111111111 + export t + i=1$i + if test $i = 1111111111111111111111111111111111111111111111; then i=2; fi + if test $i = 1111111111111111111111111111111111111111111112; then i=3; fi + if test $i = 1111111111111111111111111111111111111111111113; then i=4; fi + if test $i = 1111111111111111111111111111111111111111111114; then i=5; fi + if test $i = 1111111111111111111111111111111111111111111115; then i=6; fi + if test $i = 1111111111111111111111111111111111111111111116; then i=7; fi + if test $i = 1111111111111111111111111111111111111111111117; then i=8; fi + if test $i = 1111111111111111111111111111111111111111111118; then i=9; fi + if test $i = 1111111111111111111111111111111111111111111119; then i=a; fi + if test $i = 111111111111111111111111111111111111111111111a; then i=b; fi + if test $i = 111111111111111111111111111111111111111111111b; then i=c; fi + if test $i = 111111111111111111111111111111111111111111111c; then i=d; fi + if test $i = 111111111111111111111111111111111111111111111d; then i=e; fi + if test $i = 111111111111111111111111111111111111111111111e; then i=f; fi + if test $i = 111111111111111111111111111111111111111111111f; then i=g; fi + if test $i = 111111111111111111111111111111111111111111111g; then i=h; fi + if test $i = 111111111111111111111111111111111111111111111h; then i=i; fi + if test $i = 111111111111111111111111111111111111111111111i; then i=j; fi + if test $i = 111111111111111111111111111111111111111111111j; then i=X; fi +done +end=`ps -o pid,vsz | grep "^ *$pid "` + +if test "$beg" != "$end"; then + echo "vsz grows: $beg -> $end" +else + echo "vsz does not grow" +fi