I have solved the leak problem. This patch (to busybox-1.37.0) solves the
leaks when compiled with NOMMU
(change the #define BUILD_AS_NOMMU line from 0 to 1 in order to demonstate
that the unpatch code leaks,
while the patched code does not on a system that actually has an MMU since
most developers don't have
an available system that actually lacks an MMU. This code in unpatched hush
with NO_MMU will cause the
leak that can be seen with top:

while true; do v=$(echo 0); done

Below is the patch that only fixes the leak.
------------------------------------------------  diff
-----------------------------------------------------
-- hush.c.orig 2025-07-28 17:57:34.808579711 -0400
+++ hush.c      2025-08-01 15:24:14.885718809 -0400
@@ -961,6 +961,9 @@
 #if ENABLE_HUSH_GETOPTS
        unsigned getopt_count;
 #endif
+#if ENABLE_HUSH_TICK
+       char **to_free;
+#endif
        const char *ifs;
        char *ifs_whitespace; /* = G.ifs or malloced */
        const char *cwd;
@@ -2128,6 +2131,9 @@
                        cur_var = cur_var->next;
                        free(tmp);
                }
+               free(G.to_free);
+               G.to_free = NULL;
+               free(ptr_to_globals);
        }
 #endif

@@ -7657,9 +7663,6 @@
 {
        pid_t pid;
        int channel[2];
-# if !BB_MMU
-       char **to_free = NULL;
-# endif

        xpipe(channel);
        pid = BB_MMU ? xfork() : xvfork();
@@ -7736,7 +7739,7 @@
         * huge=`cat BIG` # was blocking here forever
         * echo OK
         */
-               re_execute_shell(&to_free,
+               re_execute_shell(&G.to_free,
                                s,
                                G.global_argv[0],
                                G.global_argv + 1,
@@ -7754,7 +7757,8 @@
 # endif
        enable_restore_tty_pgrp_on_exit();
 # if !BB_MMU
-       free(to_free);
+       free(G.to_free);
+       G.to_free = NULL;

--------------------------------------------------------------------------------------------------

In order to discover these bugs, I had to greatly improve the LEAK_HUNTING
code in hush.c
as well as make a different, better hush_leaktool.sh.

Below is the patch that includes the improvements to LEAK_HUNTING as well
as the bug fix.

------------------------------------------ diff
--------------------------------------------------------
--- hush.c.orig 2025-08-01 13:31:24.541695883 -0400
+++ hush.c 2025-08-01 13:37:44.957697171 -0400
@@ -961,6 +961,9 @@
 #if ENABLE_HUSH_GETOPTS
  unsigned getopt_count;
 #endif
+#if ENABLE_HUSH_TICK
+ char **to_free;
+#endif
  const char *ifs;
  char *ifs_whitespace; /* = G.ifs or malloced */
  const char *cwd;
@@ -1343,36 +1346,157 @@
 #endif


+static char **add_strings_to_strings(char **strings, char **add, int
need_to_dup)
+{
+ int i;
+ unsigned count1;
+ unsigned count2;
+ char **v;
+
+ v = strings;
+ count1 = 0;
+ if (v) {
+ while (*v) {
+ count1++;
+ v++;
+ }
+ }
+ count2 = 0;
+ v = add;
+ while (*v) {
+ count2++;
+ v++;
+ }
+ v = xrealloc(strings, (count1 + count2 + 1) * sizeof(char*));
+ v[count1 + count2] = NULL;
+ i = count2;
+ while (--i >= 0)
+ v[count1 + i] = (need_to_dup ? xstrdup(add[i]) : add[i]);
+ return v;
+}
+
+/* Note: takes ownership of "add" ptr (it is not strdup'ed) */
+static char **add_string_to_strings(char **strings, char *add)
+{
+ char *v[2];
+ v[0] = add;
+ v[1] = NULL;
+ return add_strings_to_strings(strings, v, /*dup:*/ 0);
+}
+
 /* Leak hunting. Use hush_leaktool.sh for post-processing.
  */
+
 #if LEAK_HUNTING
-static void *xxmalloc(int lineno, size_t size)
+static char **xxadd_strings_to_strings(int lineno, char **strings, char
**add, int need_to_dup)
+{
+ char **orig = strings;
+ char **ptr = add_strings_to_strings(strings, add, need_to_dup);
+ if (orig != ptr) {
+       fdprintf(2, "%p line %d: add_strings_to_strings\n", ptr, lineno);
+ if (orig != NULL) fdprintf(2, "free %p by add_strings_to_strings\n",
orig);
+ fflush_all();
+ }
+ return ptr;
+}
+
+static char **xxadd_string_to_strings(int lineno, char **strings, char
*add)
+{
+ char **orig = strings;
+ char **ptr = add_string_to_strings(strings, add);
+ if (ptr != orig) {
+ fdprintf(2, "%p line %d: add_string_to_strings\n", ptr, lineno);
+ if (orig != NULL) fdprintf(2, "free %p by add_string_to_string\n", orig);;
+ fflush_all();
+ }
+ return ptr;
+}
+
+static void *xxmalloc(int lineno, const char *s, size_t size)
 {
  void *ptr = xmalloc((size + 0xff) & ~0xff);
- fdprintf(2, "line %d: malloc %p\n", lineno, ptr);
+ fdprintf(2, "%p line %d: malloc(%s)\n", ptr, lineno, s);
+ fflush_all();
+ return ptr;
+}
+static void *xxzalloc(int lineno, const char *s, size_t size)
+{
+ void *ptr = xzalloc((size + 0xff) & ~0xff);
+ fdprintf(2, "%p line %d: zalloc(%s)\n", ptr, lineno, s);
+ fflush_all();
  return ptr;
 }
-static void *xxrealloc(int lineno, void *ptr, size_t size)
+static void *xxrealloc(int lineno, const char *s, void *ptr, size_t size)
 {
+ void *p = ptr;
  ptr = xrealloc(ptr, (size + 0xff) & ~0xff);
- fdprintf(2, "line %d: realloc %p\n", lineno, ptr);
+ if (p != ptr) {
+       fdprintf(2, "%p line %d: realloc(%s)\n", ptr, lineno, s);
+ if (p != NULL) fdprintf(2, "free %p by realloc\n", p);
+ fflush_all();
+ }
+ return ptr;
+}
+static void *xxrealloc_getcwd_or_warn(int lineno, char *ptr)
+{
+ char *p = ptr;
+ ptr = xrealloc_getcwd_or_warn(ptr);
+ if (p != ptr) {
+ fdprintf(2, "%p line %d: xrealloc_getcwd_or_warn\n", ptr, lineno);
+ if (p != NULL) fdprintf(2, "free %p by getcwd_or_warn\n", p);
+ fflush_all();
+ }
  return ptr;
 }
-static char *xxstrdup(int lineno, const char *str)
+static char *xxstrdup(int lineno, const char *s, const char *str)
 {
  char *ptr = xstrdup(str);
- fdprintf(2, "line %d: strdup %p\n", lineno, ptr);
+ fdprintf(2, "%p line %d: strdup(%s) %s\n", ptr, lineno, s, ptr);
+ fflush_all();
+ return ptr;
+}
+static char *xxstrndup(int lineno, const char *s, const char *str, int n)
+{
+ char *ptr = xstrndup(str, n);
+ fdprintf(2, "%p line %d: strndup(%s) %s\n", ptr, lineno, s, ptr);
+ fflush_all();
  return ptr;
 }
-static void xxfree(void *ptr)
+static char *xxasprintf(int lineno, const char *f, ...)
 {
- fdprintf(2, "free %p\n", ptr);
+ int r;
+ char *p;
+ va_list args;
+ va_start(args, f);
+ r = vasprintf(&p, f, args);
+ va_end(args);
+ if (r < 0)
+ bb_die_memory_exhausted();
+ fdprintf(2, "%p line %d: xasprintf %s\n", p, lineno, p);
+ fflush_all();
+ return p;
+}
+static void xxfree(int lineno, const char *s, void *ptr)
+{
+ if (ptr) {
+ fdprintf(2, "free %p line %d (%s)\n", ptr, lineno, s);
+ fflush_all();
  free(ptr);
+ } else fdprintf(2, "free null line %d (%s)\n", lineno, s);
 }
-# define xmalloc(s)     xxmalloc(__LINE__, s)
-# define xrealloc(p, s) xxrealloc(__LINE__, p, s)
-# define xstrdup(s)     xxstrdup(__LINE__, s)
-# define free(p)        xxfree(p)
+#define STRINGIFY(x) #x
+#define add_strings_to_strings(strings, add, need_to_dup) \
+ xxadd_strings_to_strings(__LINE__, strings, add, need_to_dup)
+#define add_string_to_strings(strings, add) \
+ xxadd_string_to_strings(__LINE__, strings, add)
+# define xmalloc(s)     xxmalloc(__LINE__, STRINGIFY(s), s)
+# define xzalloc(s)    xxzalloc(__LINE__, STRINGIFY(s), s)
+# define xrealloc(p, s) xxrealloc(__LINE__, STRINGIFY(p), p, s  )
+# define xstrdup(s)     xxstrdup(__LINE__, STRINGIFY(s), s)
+# define xstrndup(s, n) xxstrndup(__LINE__, STRINGIFY(s), s, n)
+# define xrealloc_getcwd_or_warn(p) xxrealloc_getcwd_or_warn(__LINE__, p)
+# define xasprintf(f, ...) xxasprintf(__LINE__, f, __VA_ARGS__)
+# define free(p)        xxfree(__LINE__, STRINGIFY(p), p)
 #endif


@@ -1496,64 +1620,6 @@
  return dst;
 }

-static char **add_strings_to_strings(char **strings, char **add, int
need_to_dup)
-{
- int i;
- unsigned count1;
- unsigned count2;
- char **v;
-
- v = strings;
- count1 = 0;
- if (v) {
- while (*v) {
- count1++;
- v++;
- }
- }
- count2 = 0;
- v = add;
- while (*v) {
- count2++;
- v++;
- }
- v = xrealloc(strings, (count1 + count2 + 1) * sizeof(char*));
- v[count1 + count2] = NULL;
- i = count2;
- while (--i >= 0)
- v[count1 + i] = (need_to_dup ? xstrdup(add[i]) : add[i]);
- return v;
-}
-#if LEAK_HUNTING
-static char **xx_add_strings_to_strings(int lineno, char **strings, char
**add, int need_to_dup)
-{
- char **ptr = add_strings_to_strings(strings, add, need_to_dup);
- fdprintf(2, "line %d: add_strings_to_strings %p\n", lineno, ptr);
- return ptr;
-}
-#define add_strings_to_strings(strings, add, need_to_dup) \
- xx_add_strings_to_strings(__LINE__, strings, add, need_to_dup)
-#endif
-
-/* Note: takes ownership of "add" ptr (it is not strdup'ed) */
-static char **add_string_to_strings(char **strings, char *add)
-{
- char *v[2];
- v[0] = add;
- v[1] = NULL;
- return add_strings_to_strings(strings, v, /*dup:*/ 0);
-}
-#if LEAK_HUNTING
-static char **xx_add_string_to_strings(int lineno, char **strings, char
*add)
-{
- char **ptr = add_string_to_strings(strings, add);
- fdprintf(2, "line %d: add_string_to_strings %p\n", lineno, ptr);
- return ptr;
-}
-#define add_string_to_strings(strings, add) \
- xx_add_string_to_strings(__LINE__, strings, add)
-#endif
-
 static void free_strings(char **strings)
 {
  char **v;
@@ -2128,6 +2194,10 @@
  cur_var = cur_var->next;
  free(tmp);
  }
+ free(G.to_free);
+ G.to_free = 0;
+ free(ptr_to_globals);
  }
 #endif

@@ -7658,7 +7728,7 @@
  pid_t pid;
  int channel[2];
 # if !BB_MMU
- char **to_free = NULL;
+ G.to_free = NULL;
 # endif

  xpipe(channel);
@@ -7736,7 +7806,7 @@
  * huge=`cat BIG` # was blocking here forever
  * echo OK
  */
- re_execute_shell(&to_free,
+ re_execute_shell(&G.to_free,
  s,
  G.global_argv[0],
  G.global_argv + 1,
@@ -7754,7 +7824,8 @@
 # endif
  enable_restore_tty_pgrp_on_exit();
 # if !BB_MMU
- free(to_free);
+ free(G.to_free);
+ G.to_free = NULL;
 # endif
  close(channel[1]);
  return channel[0];
____________________________________________________________________

Below is the new hush_leaktool.sh

----------------------------------------------- diff
--------------------------------------------------------------
#!/bin/bash
# hush_leaktool.sh
# works with GNU sed, maybe not busybox's sed.

# hush's stderr with leak debug enabled
output=output

grep -v '^free' "$output" > "$output.leaked"
grep '^free 0x' "$output" > "$output.spurious_free"
grep '^0x' "$output" > "$output.alloc"
alloclist=`cut -d' ' -f1 "$output.alloc" | xargs`

for mem in $alloclist; do
    if grep -q $mem "$output.spurious_free"; then
        # echo "$mem freed"
        # drop first corresponding free
        sed -i '0,/'$mem'/{/'$mem'/d}' "$output.spurious_free"
        # drop from the leaked list
        sed -i '0,/'$mem'/{/'$mem'/d}' "$output.leaked"
    fi
done
cat "$output.leaked"
_______________________________________________________________________

Finally, mea culpa in my first message: the leak had nothing to do with
looping. I didn't
realize that hush didn't treat {1..10} like bash does. Also my leak hunting
improvements
still had bugs that led to false positives (but that's fixed now), so my
clues were wrong.
I wasn't wrong about the original leak_hunting code being able to hide all
kinds of leaks.

harry
_______________________________________________
busybox mailing list
[email protected]
https://lists.busybox.net/mailman/listinfo/busybox

Reply via email to