NetBSD Problem Report #60054

From www@netbsd.org  Tue Mar  3 22:14:46 2026
Return-Path: <www@netbsd.org>
Received: from mail.netbsd.org (mail.netbsd.org [199.233.217.200])
	(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
	 key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256
	 client-signature RSA-PSS (2048 bits) client-digest SHA256)
	(Client CN "mail.netbsd.org", Issuer "R13" (verified OK))
	by mollari.NetBSD.org (Postfix) with ESMTPS id 0EDC21A9239
	for <gnats-bugs@gnats.NetBSD.org>; Tue,  3 Mar 2026 22:14:46 +0000 (UTC)
Message-Id: <20260303221444.8C8261A923C@mollari.NetBSD.org>
Date: Tue,  3 Mar 2026 22:14:44 +0000 (UTC)
From: kris@tranception.com
Reply-To: kris@tranception.com
To: gnats-bugs@NetBSD.org
Subject: libedit: adding user defined functions with narrow name/desription leaks memory
X-Send-Pr-Version: www-1.0
X-From4GNATS: "kris@tranception.com via gnats" <gnats-admin@NetBSD.org>

>Number:         60054
>Category:       lib
>Synopsis:       libedit: adding user defined functions with narrow name/desription leaks memory
>Confidential:   no
>Severity:       serious
>Priority:       medium
>Responsible:    lib-bug-people
>State:          open
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Tue Mar 03 22:15:00 +0000 2026
>Last-Modified:  Wed Mar 04 06:05:00 +0000 2026
>Originator:     Kristofer Peterson
>Release:        NetBSD-current
>Organization:
Tranception Limited
>Environment:
>Description:
Adding user defined functions in libedit via el_set(EL_ADDFN) converts the narrow character name and help/description strings to wide versions allocated on the heap which are then leaked when el_end() is called.

/bin/sh registers user defined functions with editline and will call el_init() and el_end() whenever editing and interactive modes are enabled and disabled, leaking memory as it goes.

Rather than simply converting the narrow help and description strings to wide character versions and then leaking them, this patch adds an internal function map_addfunc_n() which allocates a single buffer to hold the two widened strings as well as a next buffer pointer. A head buffer pointer is added to el_map_t to link these buffers in a singly linked list which is cleaned when map_end() is called.
>How-To-Repeat:
Create this shell script:
-------------------------
set -E
x=0
while [ $x -ne 10000 ]
do
    set +i
    set -i
    x=$((x+1))
done
-------------------------

Run the shell script:
valgrind --leak-check=full --show-leak-kinds=all /bin/sh SCRIPT.SH

You should see leaked memory similar to the following:
...
==94995== LEAK SUMMARY:
==94995==    definitely lost: 1,279,872 bytes in 19,998 blocks
==94995==    indirectly lost: 0 bytes in 0 blocks
==94995==      possibly lost: 0 bytes in 0 blocks
==94995==    still reachable: 177,996 bytes in 237 blocks
==94995==         suppressed: 104,160 bytes in 1 blocks
...
>Fix:
diff -ur a/src/lib/libedit/eln.c b/src/lib/libedit/eln.c
--- a/src/lib/libedit/eln.c     2025-12-14 13:07:40.000000000 -0500
+++ b/src/lib/libedit/eln.c     2026-03-03 16:33:23.888960000 -0500
@@ -212,23 +212,11 @@

        /* XXX: do we need to change el_func_t too? */
        case EL_ADDFN: {          /* const char *, const char *, el_func_t */
-               const char *args[2];
-               el_func_t func;
-               wchar_t **wargv;
+               const char *name = va_arg(ap, const char *);
+               const char *help = va_arg(ap, const char *);
+               el_func_t func = va_arg(ap, el_func_t);

-               args[0] = va_arg(ap, const char *);
-               args[1] = va_arg(ap, const char *);
-               func = va_arg(ap, el_func_t);
-
-               wargv = ct_decode_argv(2, args, &el->el_lgcyconv);
-               if (!wargv) {
-                   ret = -1;
-                   goto out;
-               }
-               /* XXX: The two strdup's leak */
-               ret = map_addfunc(el, wcsdup(wargv[0]), wcsdup(wargv[1]),
-                   func);
-               el_free(wargv);
+               ret = map_addfunc_n(el, name, help, func);
                break;
        }
        case EL_HIST: {           /* hist_fun_t, const char * */
diff -ur a/src/lib/libedit/map.c b/src/lib/libedit/map.c
--- a/src/lib/libedit/map.c     2025-12-14 13:07:40.000000000 -0500
+++ b/src/lib/libedit/map.c     2026-03-03 16:47:20.423101000 -0500
@@ -57,6 +57,11 @@
 #include "help.h"
 #include "parse.h"

+typedef struct el_map_data_t {
+       struct el_map_data_t    *next;
+       wchar_t                  data[];
+}  el_map_data_t;
+
 static void    map_print_key(EditLine *, el_action_t *, const wchar_t *);
 static void    map_print_some_keys(EditLine *, el_action_t *, wint_t, wint_t);
 static void    map_print_all_keys(EditLine *);
@@ -934,6 +939,7 @@
            * EL_NUM_FCNS);
        el->el_map.nfunc = EL_NUM_FCNS;
        el->el_map.wordchars = NULL;
+       el->el_map.data = NULL;

 #ifdef VIDEFAULT
        map_init_vi(el);
@@ -966,6 +972,12 @@
        el->el_map.help = NULL;
        el_free(el->el_map.func);
        el->el_map.func = NULL;
+       for (el_map_data_t *curr = (el_map_data_t *)el->el_map.data; curr != NULL;) {
+               el_map_data_t * const next = curr->next;
+               el_free(curr);
+               curr = next;
+       }
+       el->el_map.data = NULL;
 }


@@ -1424,6 +1436,40 @@
        default:
                EL_ABORT((el->el_errfile, "Bad XK_ type %d\n", ntype));
        }
+       return 0;
+}
+
+
+/* map_addfunc():
+ *     add a user defined function widening narrow name & help strings
+ */
+libedit_private int
+map_addfunc_n(EditLine *el, const char *name, const char *help,
+    el_func_t func)
+{
+       el_map_data_t *data;
+       wchar_t *name_w, *help_w;
+       size_t name_n, help_n;
+
+       if (name == NULL || help == NULL || func == NULL ||
+           (name_n = mbstowcs(NULL, name, 0)) == (size_t)-1 ||
+           (help_n = mbstowcs(NULL, help, 0)) == (size_t)-1 ||
+           (data = el_malloc(sizeof(*data) + sizeof(wchar_t) *
+           (name_n + help_n + 2))) == NULL)
+               return -1;
+
+       name_w = data->data;
+       help_w = data->data + name_n + 1;
+
+       if (mbstowcs(name_w, name, name_n + 1) > name_n ||
+           mbstowcs(help_w, help, help_n + 1) > help_n ||
+           map_addfunc(el, name_w, help_w, func) != 0) {
+               el_free(data);
+               return -1;
+       }
+
+       data->next = el->el_map.data;
+       el->el_map.data = data;
        return 0;
 }

diff -ur a/src/lib/libedit/map.h b/src/lib/libedit/map.h
--- a/src/lib/libedit/map.h     2025-12-14 13:07:40.000000000 -0500
+++ b/src/lib/libedit/map.h     2026-03-03 16:35:30.839826000 -0500
@@ -60,6 +60,7 @@
        el_func_t       *func;          /* List of available functions  */
        size_t           nfunc;         /* The number of functions/help items */
        wchar_t         *wordchars;     /* The word character separators */
+       void            *data;          /* Widened string data for functions */
 } el_map_t;

 #define        MAP_EMACS       0
@@ -77,6 +78,8 @@
 libedit_private int    map_set_wordchars(EditLine *, wchar_t *);
 libedit_private int    map_get_wordchars(EditLine *, const wchar_t **);
 libedit_private int    map_addfunc(EditLine *, const wchar_t *, const wchar_t *,
+    el_func_t);
+libedit_private int    map_addfunc_n(EditLine *, const char *, const char *,
     el_func_t);

 #endif /* _h_el_map */

>Audit-Trail:
From: Christos Zoulas <christos@zoulas.com>
To: gnats-bugs@netbsd.org
Cc: lib-bug-people@netbsd.org,
 gnats-admin@netbsd.org,
 netbsd-bugs@netbsd.org
Subject: Re: lib/60054: libedit: adding user defined functions with narrow
 name/desription leaks memory
Date: Tue, 3 Mar 2026 17:18:35 -0500

 --Apple-Mail=_0DE034AA-DA2B-477C-B795-AA48136AD9B6
 Content-Transfer-Encoding: quoted-printable
 Content-Type: text/plain;
 	charset=us-ascii

 Isn't it simpler to always add strdup() (even in the wide case), and =
 then always free the strings?

 christos=

 --Apple-Mail=_0DE034AA-DA2B-477C-B795-AA48136AD9B6
 Content-Transfer-Encoding: 7bit
 Content-Disposition: attachment;
 	filename=signature.asc
 Content-Type: application/pgp-signature;
 	name=signature.asc
 Content-Description: Message signed with OpenPGP

 -----BEGIN PGP SIGNATURE-----
 Comment: GPGTools - http://gpgtools.org

 iF0EARECAB0WIQS+BJlbqPkO0MDBdsRxESqxbLM7OgUCaadeOwAKCRBxESqxbLM7
 OqbKAKCvEIgWP+mNVTTsAY8mpaMtC/1v2ACfS7ElLoVQ4iPgzxX3N0EY5VHlrdQ=
 =Cgze
 -----END PGP SIGNATURE-----

 --Apple-Mail=_0DE034AA-DA2B-477C-B795-AA48136AD9B6--

From: "Christos Zoulas" <christos@netbsd.org>
To: gnats-bugs@gnats.NetBSD.org
Cc: 
Subject: PR/60054 CVS commit: src/lib/libedit
Date: Tue, 3 Mar 2026 18:04:02 -0500

 Module Name:	src
 Committed By:	christos
 Date:		Tue Mar  3 23:04:02 UTC 2026

 Modified Files:
 	src/lib/libedit: eln.c makelist map.c map.h

 Log Message:
 PR/60054: Kristofer Peterson: adding user defined functions with narrow
 name/desription leaks memory


 To generate a diff of this commit:
 cvs rdiff -u -r1.39 -r1.40 src/lib/libedit/eln.c
 cvs rdiff -u -r1.29 -r1.30 src/lib/libedit/makelist
 cvs rdiff -u -r1.57 -r1.58 src/lib/libedit/map.c
 cvs rdiff -u -r1.14 -r1.15 src/lib/libedit/map.h

 Please note that diffs are not public domain; they are subject to the
 copyright notices on the relevant files.

From: Kristofer Peterson <kris@tranception.com>
To: "gnats-bugs" <gnats-bugs@netbsd.org>
Cc: "lib-bug-people" <lib-bug-people@netbsd.org>,
	"gnats-admin" <gnats-admin@netbsd.org>,
	"netbsd-bugs" <netbsd-bugs@netbsd.org>
Subject: Re: lib/60054: libedit: adding user defined functions with narrow
 name/desription leaks memory
Date: Wed, 04 Mar 2026 04:15:43 +0000

 ------=_Part_1051427_1479726595.1772597743656
 Content-Type: text/plain; charset="UTF-8"
 Content-Transfer-Encoding: quoted-printable

 I originally looked at wcsdup()'ing then freeing each name/description in m=
 ap_end.

 However the builtin functions all have static strings so can't be freed.

 So then I looked at just skipping the first=C2=A0EL_NUM_FCNS entries and fr=
 eeing the rest.



 Then I saw with el_wset(EL_ADDFN) there was no wcsdup() so I gathered the i=
 nterface was supposed to use the passed in strings, as a library user could=
  also just have static wstrings for their commands so no point in copying t=
 hose. I looked at the man page but it wasn't clear whether EL_ADDFN was sup=
 posed to copy the strings or whether the caller had to guarantee the storag=
 e lifetime until el_end().



 So then I settled on the list of buffers which are only used when narrow na=
 me/descriptions strings are passed.



 But always copying strings whether narrow or wide would also work (perhaps =
 skipping the first EL_NUM_FCNS builtin functions).

 Freeing a const char* or const wchar_t* doesn't work though with the compil=
 er warnings, you need an ugly double cast (i.e. free((void*)(intptr_t)ptr))=
 .


 -K







 From: Christos Zoulas via gnats <gnats-admin@NetBSD.org>
 To: <lib-bug-people@netbsd.org>, <gnats-admin@netbsd.org>, <netbsd-bugs@net=
 bsd.org>, <kris@tranception.com>
 Date: Tue, 03 Mar 2026 22:20:03 +0000
 Subject: Re: lib/60054: libedit: adding user defined functions with narrow =
 name/desription leaks memory



 The following reply was made to PR lib/60054; it has been noted by GNATS.=
 =20
 =20
 From: Christos Zoulas < mailto:christos@zoulas.com >=20
 To: mailto:gnats-bugs@netbsd.org =20
 Cc: mailto:lib-bug-people@netbsd.org ,=20
  mailto:gnats-admin@netbsd.org ,=20
  mailto:netbsd-bugs@netbsd.org =20
 Subject: Re: lib/60054: libedit: adding user defined functions with narrow=
 =20
  name/desription leaks memory=20
 Date: Tue, 3 Mar 2026 17:18:35 -0500=20
 =20
  --Apple-Mail=3D_0DE034AA-DA2B-477C-B795-AA48136AD9B6=20
  Content-Transfer-Encoding: quoted-printable=20
  Content-Type: text/plain;=20
  =C2=A0=C2=A0=C2=A0=C2=A0charset=3Dus-ascii=20
 =20
  Isn't it simpler to always add strdup() (even in the wide case), and =3D=
 =20
  then always free the strings?=20
 =20
  christos=3D=20
 =20
  --Apple-Mail=3D_0DE034AA-DA2B-477C-B795-AA48136AD9B6=20
  Content-Transfer-Encoding: 7bit=20
  Content-Disposition: attachment;=20
  =C2=A0=C2=A0=C2=A0=C2=A0filename=3Dsignature.asc=20
  Content-Type: application/pgp-signature;=20
  =C2=A0=C2=A0=C2=A0=C2=A0name=3Dsignature.asc=20
  Content-Description: Message signed with OpenPGP=20
 =20
  -----BEGIN PGP SIGNATURE-----=20
  Comment: GPGTools - http://gpgtools.org =20
 =20
  iF0EARECAB0WIQS+BJlbqPkO0MDBdsRxESqxbLM7OgUCaadeOwAKCRBxESqxbLM7=20
  OqbKAKCvEIgWP+mNVTTsAY8mpaMtC/1v2ACfS7ElLoVQ4iPgzxX3N0EY5VHlrdQ=3D=20
  =3DCgze=20
  -----END PGP SIGNATURE-----=20
 =20
  --Apple-Mail=3D_0DE034AA-DA2B-477C-B795-AA48136AD9B6--
 ------=_Part_1051427_1479726595.1772597743656
 Content-Type: text/html; charset="UTF-8"
 Content-Transfer-Encoding: quoted-printable

 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head>=
 <meta content=3D"text/html;charset=3DUTF-8" http-equiv=3D"Content-Type"></h=
 ead><body ><div style=3D"font-family: Verdana, Arial, Helvetica, sans-serif=
 ; font-size: 10pt;"><div>I originally looked at wcsdup()'ing then freeing e=
 ach name/description in map_end.<br></div><div>However the builtin function=
 s all have static strings so can't be freed.<br></div><div>So then I looked=
  at just skipping the first&nbsp;EL_NUM_FCNS entries and freeing the rest.<=
 br></div><div><br></div><div>Then I saw with el_wset(EL_ADDFN) there was no=
  wcsdup() so I gathered the interface was supposed to use the passed in str=
 ings, as a library user could also just have static wstrings for their comm=
 ands so no point in copying those. I looked at the man page but it wasn't c=
 lear whether EL_ADDFN was supposed to copy the strings or whether the calle=
 r had to guarantee the storage lifetime until el_end().<br></div><div><br><=
 /div><div>So then I settled on the list of buffers which are only used when=
  narrow name/descriptions strings are passed.<br></div><div><br></div><div>=
 But always copying strings whether narrow or wide would also work (perhaps =
 skipping the first EL_NUM_FCNS builtin functions).<br></div><div>Freeing a =
 const char* or const wchar_t* doesn't work though with the compiler warning=
 s, you need an ugly double cast (i.e. free((void*)(intptr_t)ptr)).</div><di=
 v><br></div><div>-K<br></div><div><br></div><div class=3D"zmail_extra_hr" s=
 tyle=3D"border-top: 1px solid rgb(204, 204, 204); height: 0px; margin-top: =
 10px; margin-bottom: 10px; line-height: 0px;"><br></div><div class=3D"zmail=
 _extra" data-zbluepencil-ignore=3D"true"><div><br></div><div id=3D"Zm-_Id_-=
 Sgn1">From: Christos Zoulas via gnats &lt;gnats-admin@NetBSD.org&gt;<br>To:=
  &lt;lib-bug-people@netbsd.org&gt;, &lt;gnats-admin@netbsd.org&gt;, &lt;net=
 bsd-bugs@netbsd.org&gt;, &lt;kris@tranception.com&gt;<br>Date: Tue, 03 Mar =
 2026 22:20:03 +0000<br>Subject: Re: lib/60054: libedit: adding user defined=
  functions with narrow name/desription leaks memory<br></div><div><br></div=
 ><blockquote id=3D"blockquote_zmail" style=3D"margin: 0px;"><div>The follow=
 ing reply was made to PR lib/60054; it has been noted by GNATS. <br> <br>Fr=
 om: Christos Zoulas &lt;<a target=3D"_blank" href=3D"mailto:christos@zoulas=
 .com">christos@zoulas.com</a>&gt; <br>To: <a target=3D"_blank" href=3D"mail=
 to:gnats-bugs@netbsd.org">gnats-bugs@netbsd.org</a> <br>Cc: <a target=3D"_b=
 lank" href=3D"mailto:lib-bug-people@netbsd.org">lib-bug-people@netbsd.org</=
 a>, <br> <a target=3D"_blank" href=3D"mailto:gnats-admin@netbsd.org">gnats-=
 admin@netbsd.org</a>, <br> <a target=3D"_blank" href=3D"mailto:netbsd-bugs@=
 netbsd.org">netbsd-bugs@netbsd.org</a> <br>Subject: Re: lib/60054: libedit:=
  adding user defined functions with narrow <br> name/desription leaks memor=
 y <br>Date: Tue, 3 Mar 2026 17:18:35 -0500 <br> <br> --Apple-Mail=3D_0DE034=
 AA-DA2B-477C-B795-AA48136AD9B6 <br> Content-Transfer-Encoding: quoted-print=
 able <br> Content-Type: text/plain; <br> &nbsp;&nbsp;&nbsp;&nbsp;charset=3D=
 us-ascii <br> <br> Isn't it simpler to always add strdup() (even in the wid=
 e case), and =3D <br> then always free the strings? <br> <br> christos=3D <=
 br> <br> --Apple-Mail=3D_0DE034AA-DA2B-477C-B795-AA48136AD9B6 <br> Content-=
 Transfer-Encoding: 7bit <br> Content-Disposition: attachment; <br> &nbsp;&n=
 bsp;&nbsp;&nbsp;filename=3Dsignature.asc <br> Content-Type: application/pgp=
 -signature; <br> &nbsp;&nbsp;&nbsp;&nbsp;name=3Dsignature.asc <br> Content-=
 Description: Message signed with OpenPGP <br> <br> -----BEGIN PGP SIGNATURE=
 ----- <br> Comment: GPGTools - <a target=3D"_blank" href=3D"http://gpgtools=
 .org">http://gpgtools.org</a> <br> <br> iF0EARECAB0WIQS+BJlbqPkO0MDBdsRxESq=
 xbLM7OgUCaadeOwAKCRBxESqxbLM7 <br> OqbKAKCvEIgWP+mNVTTsAY8mpaMtC/1v2ACfS7El=
 LoVQ4iPgzxX3N0EY5VHlrdQ=3D <br> =3DCgze <br> -----END PGP SIGNATURE----- <b=
 r> <br> --Apple-Mail=3D_0DE034AA-DA2B-477C-B795-AA48136AD9B6-- <br> <br></d=
 iv></blockquote></div><div><br></div></div><br></body></html>
 ------=_Part_1051427_1479726595.1772597743656--

NetBSD Home
NetBSD PR Database Search

(Contact us) $NetBSD: query-full-pr,v 1.51 2026/08/10 02:28:17 riastradh Exp $
$NetBSD: gnats_config.sh,v 1.10 2026/05/13 22:00:09 riastradh Exp $
Copyright © 1994-2026 The NetBSD Foundation, Inc. ALL RIGHTS RESERVED.