git: b9cdb4d34bc4 - stable/13 - sh: Fix a double free in a rare scenario with pipes

From: Jilles Tjoelker <jilles_at_FreeBSD.org>
Date: Sun, 01 Feb 2026 21:08:41 UTC
The branch stable/13 has been updated by jilles:

URL: https://cgit.FreeBSD.org/src/commit/?id=b9cdb4d34bc4ea0093597f2bc4a86ce6ed5ab569

commit b9cdb4d34bc4ea0093597f2bc4a86ce6ed5ab569
Author:     Jilles Tjoelker <jilles@FreeBSD.org>
AuthorDate: 2025-11-15 16:43:03 +0000
Commit:     Jilles Tjoelker <jilles@FreeBSD.org>
CommitDate: 2026-02-01 14:35:54 +0000

    sh: Fix a double free in a rare scenario with pipes
    
    The command
      sh -c 'sleep 3 | sleep 2 & sleep 3 & kill %1; wait %1'
    crashes (with appropriate sanitization such as putting
    MALLOC_CONF=abort:true,junk:true in the environment or compiling with
    -fsanitize=address).
    
    What happens here is that waitcmdloop() calls dowait() with a NULL job
    pointer, instructing dowait() to freejob() if it's a non-interactive
    shell and $! was not and cannot be referenced for it. However,
    waitcmdloop() then uses fields possibly freed by freejob() and calls
    freejob() again.
    
    This only occurs if the job being waited for is identified via % syntax
    ($! has never been referenced for it), it is a pipeline with two or more
    elements and another background job has been started before the wait
    command. That seems special enough for a bug to remain. Test scripts
    written by Jilles would almost always use $! and not % syntax.
    
    We can instead make waitcmdloop() pass its job pointer to dowait(),
    fixing up things for that (waitcmdloop() will have to call deljob() if
    it does not call freejob()).
    
    The crash from
    https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=290330#c2 appears to
    be the same bug.
    
    PR:             290330
    Reported by:    bdrewery
    Reviewed by:    bdrewery
    Differential Revision:  https://reviews.freebsd.org/D53773
    
    (cherry picked from commit 75a6c38e4d5c651b7398bf2bea5baa41a0939e92)
---
 bin/sh/jobs.c                  | 3 ++-
 bin/sh/tests/builtins/Makefile | 1 +
 bin/sh/tests/builtins/wait11.0 | 6 ++++++
 3 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/bin/sh/jobs.c b/bin/sh/jobs.c
index e7d14158b555..d5d8ca978bb5 100644
--- a/bin/sh/jobs.c
+++ b/bin/sh/jobs.c
@@ -579,6 +579,7 @@ waitcmdloop(struct job *job)
 					freejob(job);
 				else {
 					job->remembered = 0;
+					deljob(job);
 					if (job == bgjob)
 						bgjob = NULL;
 				}
@@ -603,7 +604,7 @@ waitcmdloop(struct job *job)
 					break;
 			}
 		}
-	} while (dowait(DOWAIT_BLOCK | DOWAIT_SIG, (struct job *)NULL) != -1);
+	} while (dowait(DOWAIT_BLOCK | DOWAIT_SIG, job) != -1);
 
 	sig = pendingsig_waitcmd;
 	pendingsig_waitcmd = 0;
diff --git a/bin/sh/tests/builtins/Makefile b/bin/sh/tests/builtins/Makefile
index 5ad9216b4da0..a0c92a78664e 100644
--- a/bin/sh/tests/builtins/Makefile
+++ b/bin/sh/tests/builtins/Makefile
@@ -186,5 +186,6 @@ ${PACKAGE}FILES+=		wait7.0
 ${PACKAGE}FILES+=		wait8.0
 ${PACKAGE}FILES+=		wait9.127
 ${PACKAGE}FILES+=		wait10.0
+${PACKAGE}FILES+=		wait11.0
 
 .include <bsd.test.mk>
diff --git a/bin/sh/tests/builtins/wait11.0 b/bin/sh/tests/builtins/wait11.0
new file mode 100644
index 000000000000..d5fab26fb677
--- /dev/null
+++ b/bin/sh/tests/builtins/wait11.0
@@ -0,0 +1,6 @@
+sleep 3 | sleep 2 &
+sleep 3 &
+kill %1
+wait %1
+r=$?
+[ "$r" -gt 128 ] && [ "$(kill -l "$r")" = TERM ]