Skip to content

Fix #947: memory leak invoking subshells/functions with trap set in parent environment - #948

Merged
McDutchie merged 6 commits into
ksh93:devfrom
McDutchie:issue-947
Mar 20, 2026
Merged

McDutchie merged 6 commits into
ksh93:devfrom
McDutchie:issue-947

Conversation

@McDutchie

Copy link
Copy Markdown

Vincent Mihalkovič (@vmihalko) from Red Hat reported a memory leak. See #947.

Analysis:

The Red Hat patch backported back in 2020, in 6193c6a, is not correct. The original 93u+ code mostly made sense; there is no reason why saved trap action pointers should change in subshells or ksh function invocations, because we're creating a new scope and any changes in traps apply to the new scope only, so the original pointers should never be invalidated. Any such occurrence is a bug.

The Red Hat patch doesn't actually fix any such bug; it papers over it at the expense of introducing a memory leak problem. So we have to revert it, then figure out what the actual bug is and find a proper fix for that bug.

src/cmd/ksh93/sh/subshell.c,
src/cmd/ksh93/sh/xec.c:

  • Mostly revert to the 93u+ method for saving/restoring trap table, which simply copies over the array of pointers.
  • Avoid reintroducing the off-by-one error that was fixed in 3aee10d and which carried over into the Red Hat patch.
  • sh_subshell(): Allocate the buffer for saving the trap action pointers on the AST stack, which is automatically reset appropriately at the end of every recursive sh_exec() run, which removes the need to free the buffer manually while guaranteeing the buffer stays available while we need it. (Note that sh_funscope() was already using this method in 93u+.)

With these changes applied, the memory leak problem disappears, and the regression tests appear to pass normally. However, when compiling with AddressSanitizer, the patch fails one of the two regression tests that were introduced by referenced commit, the one that tests a ksh function. This is not unexpected. The failing test is at src/cmd/ksh93/tests/functions.sh, lines 1368-1383.

(The other one, at subshell.sh lines 932-943, passes, because as of 3d1a472, ksh always forks a subshell when it traps a signal, which circumvents the bug.)

The regression is caused by a use after free. The offending free call is in b_trap(), line 180. That code is essentially unchanged since 93u+. This use after free appears to be the very problem that the Red Hat patch from the referenced commit works around.

So we need a new and correct solution for that use after free. It occurs when a trap for a signal is set or ignored in the parent environment and is then set again in a ksh function. The function then needs to be called at twice for the use after free to occur. Reproducer:

trap "" HUP
function ksh_fun
{
	trap ": foo" HUP
	trap ": bar" HUP
}
ksh_fun
ksh_fun

A simplistic fix would be to avoid the free call at trap.c:180 if we're currently inside a ksh function. But that would introduce a memory leak when the same signal is trapped locally more than once in the same ksh function.

The challenge is this: The first time a trap is set in a ksh function scope, the parent trap should not be freed, because it will be restored. However, the second and subsequent times, it should be freed, because it's overwriting the first one. The following changes make that happen.

src/cmd/ksh93/include/shell.h,
src/cmd/ksh93/bltins/trap.c,
src/cmd/ksh93/sh/subshell.c,
src/cmd/ksh93/sh/xec.c:

  • Introduce a sh.st.trapnofree bitmask array, stored in the current scope sh.st, which contains a bit for every possible signal. By default, all those bits are zero and traps are freed as normal.
  • Use the NSIG macro (which indicates the number of signals supported by the system; non-standard but available on all major systems), to determine the size of the bitmask array if possible.
  • sh_subshell(), sh_funscope(), sh_exec(): When saving the array of trap action pointers, set all trapnofree bits to one.
  • b_trap(): Whenever the trap built-in would free a previous (potentially parent) trap, skip the free call if the corresponding bit is set, but then clear that bit, so that if the same signal trap is overwritten within the same scope, the free is executed as normal.

While we're here, let's fix another, more minor problem: the shell unnecessarily forks when setting an EXIT trap, as EXIT is the one pseudosignal that is handled in the same array as the signal traps.

src/cmd/ksh93/bltins/trap.c: b_trap():

  • Since EXIT == 0, check for sig > 0 before forking. (re: 3d1a472)

Resolves: #947

Vincent Mihalkovič (@vmihalko) from Red Hat reports:
> ksh leaks memory when running a loop that combines trap and
> command substitution. Without the trap or command substitution,
> the loop does not leak. The leak is reproducible on upstream
> HEAD.
>
> ### Reproducer
>
>	trap exit INT
>
>	n=10000
>	while [ "$n" -gt 0 ]; do
>		value=`echo a`
>		n=$((n - 1))
>	done
>	exit 0
>
> Steps:
>
>     1. Save as e.g. `reproducer_limited.ksh` and make executable:
>        chmod +x reproducer_limited.ksh
>     2. Run under valgrind:
>        valgrind --leak-check=full -q /path/to/ksh \
>				reproducer_limited.ksh
>     3. Inspect valgrind output for the LEAK SUMMARY and
>        "definitely lost" lines.
>
> Valgrind reports "definitely lost" blocks from the
> trap/command-substitution path. Example (upstream HEAD):
>
>     * 49,995 bytes in 9,999 blocks are definitely lost
>     * Stack points to: malloc → _ast_strdup → sh_strdup (init.c)
>       → sh_subshell (subshell.c) → comsubst (macro.c) → …
>
> Without the 'trap exit INT' line, the same loop does not show
> "definitely lost" (valgrind is clean for that path); the trap is
> required to trigger the leak.
>
> ### References
>
>     * Downstream: RHEL-19580 – ksh leak with traps
>       https://issues.redhat.com/browse/RHEL-19580
>       (Red Hat Jira). Affects: rhel-7.9, rhel-8.8.0, rhel-9.4.
>     * Related:
>       - Bugzilla 1460940
>         https://bugzilla.redhat.com/show_bug.cgi?id=1460940
>	- Bugzilla 1117404
>         https://bugzilla.redhat.com/show_bug.cgi?id=1117404
>         (trap/crash context).

Analysis:

The Red Hat patch backported back in 2020, in 6193c6a, is not
correct. The original 93u+ code mostly made sense; there is no
reason why saved trap action pointers should change in subshells or
ksh function invocations, because we're creating a new scope and
any changes in traps apply to the new scope only, so the original
pointers should never be invalidated. Any such occurrence is a bug.

The Red Hat patch doesn't actually fix any such bug; it papers over
it at the expense of introducing a memory leak problem. So we have
to revert it, then figure out what the actual bug is and find a
proper fix for that bug.

src/cmd/ksh93/sh/subshell.c,
src/cmd/ksh93/sh/xec.c:
- Mostly revert to the 93u+ method for saving/restoring trap table,
  which simply copies over the array of pointers.
- Avoid reintroducing the off-by-one error that was fixed in
  3aee10d and which carried over into the Red Hat patch.
- sh_subshell(): Allocate the buffer for saving the trap action
  pointers on the AST stack, which is automatically reset
  appropriately at the end of every recursive sh_exec() run, which
  removes the need to free the buffer manually while guaranteeing
  the buffer stays available while we need it. (Note that
  sh_funscope() was already using this method in 93u+.)

With these changes applied, the memory leak problem disappears, and
the regression tests appear to pass normally. However, when
compiling with AddressSanitizer, the patch fails one of the two
regression tests that were introduced by referenced commit, the one
that tests a ksh function. This is not unexpected. The failing test
is at src/cmd/ksh93/tests/functions.sh, lines 1368-1383.

(The other one, at subshell.sh lines 932-943, passes, because as of
3d1a472, ksh always forks a subshell when it traps a signal, which
circumvents the bug.)

The regression is caused by a use after free. The offending free
call is in b_trap(), line 180. That code is essentially unchanged
since 93u+. This use after free appears to be the very problem that
the Red Hat patch from the referenced commit works around.

So we need a new and correct solution for that use after free. It
occurs when a trap for a signal is set or ignored in the parent
environment and is then set again in a ksh function. The function
then needs to be called at twice for the use after free to occur.
Reproducer:

  trap "" HUP
  function ksh_fun
  {
	trap ": foo" HUP
	trap ": bar" HUP
  }
  ksh_fun
  ksh_fun

A simplistic fix would be to avoid the free call at trap.c:180 if
we're currently inside a ksh function. But that would introduce a
memory leak when the same signal is trapped locally more than once
in the same ksh function.

The challenge is this: The first time a trap is set in a ksh
function scope, the parent trap should not be freed, because it
will be restored. However, the second and subsequent times, it
should be freed, because it's overwriting the first one. The
following changes make that happen.

src/cmd/ksh93/include/shell.h,
src/cmd/ksh93/bltins/trap.c,
src/cmd/ksh93/sh/subshell.c,
src/cmd/ksh93/sh/xec.c:
- Introduce a sh.st.trapnofree bitmask array, stored in the current
  scope sh.st, which contains a bit for every possible signal. By
  default, all those bits are zero and traps are freed as normal.
- Use the NSIG macro (which indicates the number of signals
  supported by the system; non-standard but available on all major
  systems), to determine the size of the bitmask array if possible.
- sh_subshell(), sh_funscope(), sh_exec(): When saving the array of
  trap action pointers, set all trapnofree bits to one.
- b_trap(): Whenever the trap built-in would free a previous
  (potentially parent) trap, skip the free call if the
  corresponding bit is set, but then clear that bit, so that if the
  same signal trap is overwritten within the same scope, the free
  is executed as normal.

While we're here, let's fix another, more minor problem: the shell
unnecessarily forks when setting an EXIT trap, as EXIT is the one
pseudosignal that is handled in the same array as the signal traps.

src/cmd/ksh93/bltins/trap.c: b_trap():
- Since EXIT == 0, check for sig > 0 before forking. (re: 3d1a472)

Resolves: ksh93#947
@McDutchie
McDutchie requested a review from JohnoKing March 19, 2026 00:32
@McDutchie

Copy link
Copy Markdown
Author

@vmihalko, @JohnoKing: I would be grateful if you could test and review this pull request. Please try hard to break it.

Comment thread src/cmd/ksh93/sh/subshell.c Outdated
@McDutchie McDutchie changed the title Fix memleak invoking subshells w/ trap set (re: 6193c6a3) Fix #947: memory leak invoking subshells/functions with trap set in parent environment Mar 19, 2026
@JohnoKing

JohnoKing commented Mar 19, 2026 •

Copy link
Copy Markdown

The current version doesn't crash in my testing, though there is still some memory leaking. Test case:

trap "" HUP
function foo
{
	trap ": foo" HUP
	trap ": bar" HUP
	ksh_fun()
	(
		trap ": foo" HUP
		trap ": bar" HUP
	)
	ksh_fun
	ksh_fun
}
foo
foo

ASan output (prints 4 times, so 24 bytes total):

Direct leak of 6 byte(s) in 1 object(s) allocated from:
    #0 0x55c7d4266b05 in malloc (/home/johno/GitRepos/KornShell/ksh/arch/linux.i386-64/bin/ksh+0x18fb05) (BuildId: 03bf022af5a6d295f46542c80d1615737835be92)
    #1 0x55c7d45541bc in _ast_strdup /home/johno/GitRepos/KornShell/ksh/src/lib/libast/string/strdup.c:41:7
    #2 0x55c7d431b7f4 in sh_strdup /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/init.c:267:8
    #3 0x55c7d449fb16 in b_trap /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/bltins/trap.c:178:64
    #4 0x55c7d43ff604 in sh_exec /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/xec.c:1206:21
    #5 0x55c7d4406fbc in sh_exec /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/xec.c:1872:5
    #6 0x55c7d43ed46f in sh_subshell /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/subshell.c:688:4
    #7 0x55c7d4405936 in sh_exec /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/xec.c:1747:5
    #8 0x55c7d441ab2b in sh_funscope /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/xec.c:3029:5
    #9 0x55c7d4411c28 in sh_funct /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/xec.c:3149:2
    #10 0x55c7d44013bf in sh_exec /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/xec.c:1355:7
    #11 0x55c7d440706f in sh_exec /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/xec.c:1876:4
    #12 0x55c7d441af8a in sh_funscope /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/xec.c:3048:5
    #13 0x55c7d4411c28 in sh_funct /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/xec.c:3149:2
    #14 0x55c7d44013bf in sh_exec /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/xec.c:1355:7
    #15 0x55c7d42c2997 in exfile /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/main.c:606:4
    #16 0x55c7d42beff9 in sh_main /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/main.c:343:2
---snip---

SUMMARY: AddressSanitizer: 6 byte(s) leaked in 1 allocation(s).

The same reproducer run without the PR has more leaks (64 bytes total), so the current changes improve things nonetheless.

@McDutchie

Copy link
Copy Markdown
Author

Sigh. Any ideas on where that sh.st.trapcom[SIGHUP] pointer might be getting lost?

The ksh_fun function in the reproducer forks due to the parentheses. I can't reproduce the leak without the fork. Is there a way to tell if the leak occurs in the forked child or in the parent?

@JohnoKing

JohnoKing commented Mar 20, 2026 •

Copy link
Copy Markdown

It's the child:

$ cat /tmp/repro
ksh_fun()
(
	trap ": foo" HUP
	trap ": bar" HUP
	echo CHILD
)
function foo {
	trap ": foo" HUP
	ksh_fun
	echo PARENT
}
foo
:
$ arch/*/bin/ksh /tmp/repro
CHILD

=================================================================
==4817==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 6 byte(s) in 1 object(s) allocated from:
    #0 0x56068d375b05 in malloc (/home/johno/GitRepos/KornShell/ksh/arch/linux.i386-64/bin/ksh+0x18fb05) (BuildId: 03bf022af5a6d295f46542c80d1615737835be92)
    #1 0x56068d6631bc in _ast_strdup /home/johno/GitRepos/KornShell/ksh/src/lib/libast/string/strdup.c:41:7
    #2 0x56068d42a7f4 in sh_strdup /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/sh/init.c:267:8
    #3 0x56068d5aeb16 in b_trap /home/johno/GitRepos/KornShell/ksh/src/cmd/ksh93/bltins/trap.c:178:64
---snip---

SUMMARY: AddressSanitizer: 6 byte(s) leaked in 1 allocation(s).
PARENT

What likely happening is that the parent function's HUP trap isn't getting freed in the child after it changes in the child process (I'd bet it has the nofree bit, so it's left alone). It reminds me of the leaking file descriptor bug fix in 65df83f; resources belonging to the parent that the child doesn't end up using don't get cleaned up in forked subshells, leading to memory leaks in the forked subshells.

@JohnoKing

JohnoKing commented Mar 20, 2026 •

Copy link
Copy Markdown

Potential fix (perhaps insufficient if more scopes are involved):

diff --git a/src/cmd/ksh93/sh/subshell.c b/src/cmd/ksh93/sh/subshell.c
index dff9ee374..8fc7e6984 100644
--- a/src/cmd/ksh93/sh/subshell.c
+++ b/src/cmd/ksh93/sh/subshell.c
@@ -490,6 +490,7 @@ int sh_validate_subpwdfd(void)
 void sh_clear_subshell_pwdfd(void)
 {
 	struct subshell *sp = subshell_data;
+	int nsig;
 #if _lib_openat
 	int prevfd = sh.pwdfd;
 	while(sp)
@@ -513,6 +514,18 @@ void sh_clear_subshell_pwdfd(void)
 		sp = sp->prev;
 	}
 #endif /* _lib_openat */
+	nsig = sh.st.trapmax;
+	if(nsig)
+	{
+		int isig;
+		for(isig = 0; isig < nsig; ++isig)
+		{
+			const int index = isig / 8;
+			const uint8_t sigbit = (uint8_t)1 << isig % 8;
+			if(sh.st.trapnofree[index] & sigbit)
+				sh.st.trapnofree[index] &= ~sigbit;
+		}
+	}
 }
 
 /*

@McDutchie

Copy link
Copy Markdown
Author

What likely happening is that the parent function's HUP trap isn't getting freed in the child after it changes in the child process (I'd bet it has the nofree bit, so it's left alone).

Experimental patch (against this PR) to test that theory:

diff --git a/src/cmd/ksh93/sh/subshell.c b/src/cmd/ksh93/sh/subshell.c
index dff9ee374..7e5a3c425 100644
--- a/src/cmd/ksh93/sh/subshell.c
+++ b/src/cmd/ksh93/sh/subshell.c
@@ -170,6 +170,7 @@ void sh_subfork(void)
 	else
 	{
 		/* this is the child part of the fork */
+		memset(sh.st.trapnofree, 0, sizeof sh.st.trapnofree);
 		/*
 		 * In a virtual subshell, $RANDOM is not reseeded until it's used, so if that
 		 * hasn't happened yet, invalidate the seed. This allows sh_save_rand_seed()

It does seem to help. I don't know if it doesn't introduce a use after free somewhere else though.

@JohnoKing

Copy link
Copy Markdown

The memset patch also works.

@McDutchie

McDutchie commented Mar 20, 2026 •

Copy link
Copy Markdown
Author

It reminds me of the leaking file descriptor bug fix in 65df83f; resources belonging to the parent that the child doesn't end up using don't get cleaned up in forked subshells, leading to memory leaks in the forked subshells.

Yes. When sh_subfork() is called, the parent cleans up the virtual subshell and waits for the child. The child simply resets the subshell variables including sh.subshell, continues execution of the formerly virtual subshell in the fork, then returns to sh_subshell, where it simply exits the child process. No cleanup of parent scopes is ever done in the child, they just stay unused.

Which makes me think that the memset patch (or yours, which is a more elaborate way of doing the same thing) is probably okay. Since the parent scopes stay unused, freeing their traps shouldn't hurt.

I do think sh_subfork() is probably the more correct location for it, as sh_fork() (which calls sh_clear_subshell_pwdfd) is used for more than just forking subshells.

@McDutchie

Copy link
Copy Markdown
Author

Actually, both of our patches are wrong, because they miss the real issue.

The real issue is an error in logic of mine in the b_trap() changes in this PR.

b_trap() clears the nofree bit only if there is a freeable parent trap. But it should clear the nofree bit regardless of that. If we're setting a sh.st.trapcom trap action in the current scope, its nofree bit should simply always be cleared.

Commit coming up...

@McDutchie

Copy link
Copy Markdown
Author

So, AFAICT, that leak is gone with the latest commit, can you confirm?

@JohnoKing

Copy link
Copy Markdown

The leak is gone on my end.

@vmihalko

Copy link
Copy Markdown

Full internal test suite passed with no regressions (tested against the PR version prior to the latest commit).
The memory leak is also resolved on my end (with latest commit).
Thanks a lot for identifying the root cause—it was a fascinating read, and I hope there aren't too many more 'hidden' issues like this one ahead!

@McDutchie

Copy link
Copy Markdown
Author

Thanks for testing, both. In it goes.

@McDutchie
McDutchie merged commit a0a56a4 into ksh93:dev Mar 20, 2026
McDutchie added a commit that referenced this pull request Mar 20, 2026
Vincent Mihalkovič (@vmihalko) from Red Hat reports:
> ksh leaks memory when running a loop that combines trap and
> command substitution. Without the trap or command substitution,
> the loop does not leak. The leak is reproducible on upstream
> HEAD.
>
> ### Reproducer
>
>	trap exit INT
>
>	n=10000
>	while [ "$n" -gt 0 ]; do
>		value=`echo a`
>		n=$((n - 1))
>	done
>	exit 0
>
> Steps:
>
>     1. Save as e.g. `reproducer_limited.ksh` and make executable:
>        chmod +x reproducer_limited.ksh
>     2. Run under valgrind:
>        valgrind --leak-check=full -q /path/to/ksh \
>				reproducer_limited.ksh
>     3. Inspect valgrind output for the LEAK SUMMARY and
>        "definitely lost" lines.
>
> Valgrind reports "definitely lost" blocks from the
> trap/command-substitution path. Example (upstream HEAD):
>
>     * 49,995 bytes in 9,999 blocks are definitely lost
>     * Stack points to: malloc → _ast_strdup → sh_strdup (init.c)
>       → sh_subshell (subshell.c) → comsubst (macro.c) → …
>
> Without the 'trap exit INT' line, the same loop does not show
> "definitely lost" (valgrind is clean for that path); the trap is
> required to trigger the leak.
>
> ### References
>
>     * Downstream: RHEL-19580 – ksh leak with traps
>       https://issues.redhat.com/browse/RHEL-19580
>       (Red Hat Jira). Affects: rhel-7.9, rhel-8.8.0, rhel-9.4.
>     * Related:
>       - Bugzilla 1460940
>         https://bugzilla.redhat.com/show_bug.cgi?id=1460940
>	- Bugzilla 1117404
>         https://bugzilla.redhat.com/show_bug.cgi?id=1117404
>         (trap/crash context).

Analysis:

The Red Hat patch backported back in 2020, in 6193c6a, is not
correct. The original 93u+ code mostly made sense; there is no
reason why saved trap action pointers should change in subshells or
ksh function invocations, because we're creating a new scope and
any changes in traps apply to the new scope only, so the original
pointers should never be invalidated. Any such occurrence is a bug.

The Red Hat patch doesn't actually fix any such bug; it papers over
it at the expense of introducing a memory leak problem. So we have
to revert it, then figure out what the actual bug is and find a
proper fix for that bug.

src/cmd/ksh93/sh/subshell.c,
src/cmd/ksh93/sh/xec.c:
- Mostly revert to the 93u+ method for saving/restoring trap table,
  which simply copies over the array of pointers.
- Avoid reintroducing the off-by-one error that was fixed in
  3aee10d and which carried over into the Red Hat patch.
- sh_subshell(): Allocate the buffer for saving the trap action
  pointers on the AST stack, which is automatically reset
  appropriately at the end of every recursive sh_exec() run, which
  removes the need to free the buffer manually while guaranteeing
  the buffer stays available while we need it. (Note that
  sh_funscope() was already using this method in 93u+.)

With these changes applied, the memory leak problem disappears, and
the regression tests appear to pass normally. However, when
compiling with AddressSanitizer, the patch fails one of the two
regression tests that were introduced by referenced commit, the one
that tests a ksh function. This is not unexpected. The failing test
is at src/cmd/ksh93/tests/functions.sh, lines 1368-1383.

(The other one, at subshell.sh lines 932-943, passes, because as of
3d1a472, ksh always forks a subshell when it traps a signal, which
circumvents the bug.)

The regression is caused by a use after free. The offending free
call is in b_trap(), line 180. That code is essentially unchanged
since 93u+. This use after free appears to be the very problem that
the Red Hat patch from the referenced commit works around.

So we need a new and correct solution for that use after free. It
occurs when a trap for a signal is set or ignored in the parent
environment and is then set again in a ksh function. The function
then needs to be called at twice for the use after free to occur.
Reproducer:

  trap "" HUP
  function ksh_fun
  {
	trap ": foo" HUP
	trap ": bar" HUP
  }
  ksh_fun
  ksh_fun

A simplistic fix would be to avoid the free call at trap.c:180 if
we're currently inside a ksh function. But that would introduce a
memory leak when the same signal is trapped locally more than once
in the same ksh function.

The challenge is this: The first time a trap is set in a ksh
function scope, the parent trap should not be freed, because it
will be restored. However, the second and subsequent times, it
should be freed, because it's overwriting the first one. The
following changes make that happen.

src/cmd/ksh93/include/shell.h,
src/cmd/ksh93/bltins/trap.c,
src/cmd/ksh93/sh/subshell.c,
src/cmd/ksh93/sh/xec.c:
- Introduce a sh.st.trapnofree bitmask array, stored in the current
  scope sh.st, which contains a bit for every possible signal. By
  default, all those bits are zero and traps are freed as normal.
- Use the NSIG macro (which indicates the number of signals
  supported by the system; non-standard but available on all major
  systems), to determine the size of the bitmask array if possible.
- sh_subshell(), sh_funscope(), sh_exec(): When saving the array of
  trap action pointers, set all trapnofree bits to one.
- b_trap(): Whenever the trap built-in would free a previous
  (potentially parent) trap, skip the free call if the
  corresponding bit is set, but then (whether a free was actually
  executed or not) clear that bit, so that if the same signal trap
  is overwritten within the same scope, the free is executed as
  normal. This fixes the use after free while avoiding the
  introduction of another memory leak.

While we're here, let's fix another, more minor problem: the shell
unnecessarily forks when setting an EXIT trap, as EXIT is the one
pseudosignal that is handled in the same array as the signal traps.

src/cmd/ksh93/bltins/trap.c: b_trap():
- Since EXIT == 0, check for sig > 0 before forking. (re: 50db00e)

Resolves: #947
@McDutchie
McDutchie deleted the issue-947 branch March 20, 2026 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ksh memory leak with traps

3 participants