From ebada311aaf6393b027f9b12ae835626dea6762c Mon Sep 17 00:00:00 2001
From: Ethan Pini <ethan@pini.dev>
Date: Sat, 27 Jun 2026 20:19:23 -0700
Subject: [PATCH 2/3] add regression test for pipe capacity behavior on Mac

---
 Makefile.in           |   8 ++-
 support/Makefile.in   |   2 +-
 support/fillpipekva.c | 140 ++++++++++++++++++++++++++++++++++++++++++
 tests/heredoc.right   |  10 ++-
 tests/heredoc.tests   |   3 +
 tests/heredoc11.sub   |  43 +++++++++++++
 6 files changed, 202 insertions(+), 4 deletions(-)
 create mode 100644 support/fillpipekva.c
 create mode 100644 tests/heredoc11.sub

diff --git a/Makefile.in b/Makefile.in
index c4a882f7..a55a540e 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -595,9 +595,10 @@ PO_DIR = $(dot)/po/
 SUPPORT_SRC = $(srcdir)/support/
 SUPPORT_DIR = $(dot)/support
 
-TESTS_SUPPORT = recho$(EXEEXT) zecho$(EXEEXT) printenv$(EXEEXT) xcase$(EXEEXT)
+TESTS_SUPPORT = recho$(EXEEXT) zecho$(EXEEXT) printenv$(EXEEXT) xcase$(EXEEXT) \
+		  fillpipekva$(EXEEXT)
 CREATED_SUPPORT = signames.h recho$(EXEEXT) zecho$(EXEEXT) printenv$(EXEEXT) \
-		  tests/recho$(EXEEXT) tests/zecho$(EXEEXT) \
+		  tests/recho$(EXEEXT) tests/zecho$(EXEEXT) tests/fillpipekva$(EXEEXT) \
 		  tests/printenv$(EXEEXT) xcase$(EXEEXT) tests/xcase$(EXEEXT) \
 		  mksignames$(EXEEXT) lsignames.h \
 		  mksyntax${EXEEXT} syntax.c $(VERSPROG) $(VERSOBJ) \
@@ -1076,6 +1077,9 @@ printenv$(EXEEXT):	$(SUPPORT_SRC)printenv.c
 xcase$(EXEEXT):	$(SUPPORT_SRC)xcase.c
 	@$(CC_FOR_BUILD) $(CCFLAGS_FOR_BUILD) ${LDFLAGS_FOR_BUILD} -o $@ $(SUPPORT_SRC)xcase.c ${LIBS_FOR_BUILD}
 
+fillpipekva$(EXEEXT):	$(SUPPORT_SRC)fillpipekva.c
+	@$(CC_FOR_BUILD) $(CCFLAGS_FOR_BUILD) ${LDFLAGS_FOR_BUILD} -o $@ $(SUPPORT_SRC)fillpipekva.c ${LIBS_FOR_BUILD}
+
 test tests check:	force $(Program) $(TESTS_SUPPORT)
 	@-test -d tests || mkdir tests
 	@cp $(TESTS_SUPPORT) tests
diff --git a/support/Makefile.in b/support/Makefile.in
index 28fa610e..4dc9a502 100644
--- a/support/Makefile.in
+++ b/support/Makefile.in
@@ -2,7 +2,7 @@
 # Simple Makefile for the support programs.
 #
 # documentation support: man2html
-# testing support: printenv recho zecho xcase
+# testing support: printenv recho zecho xcase fillpipekva
 #
 # bashbug.sh lives here (created by configure), but bashbug is created by
 # the top-level makefile
diff --git a/support/fillpipekva.c b/support/fillpipekva.c
new file mode 100644
index 00000000..327e1b71
--- /dev/null
+++ b/support/fillpipekva.c
@@ -0,0 +1,140 @@
+/* fillpipekva.c -- push amountpipekva past maxpipekva on macOS. */
+
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This file is part of GNU Bash, the Bourne Again SHell.
+
+   Bash is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   Bash is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with Bash.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifdef MACOSX
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+
+/* Defined in apple-oss-distributions/xnu @ ff8ca1ea.
+   bsd/sys/pipe.h:74       -- `#define PIPE_SIZE 16384`
+   bsd/kern/sys_pipe.c:307 -- `PIPE_SIZE * 4`
+   */
+#define XNU_PIPESIZE_BLOCKS_MAX (1024*64)
+
+/* Defined in apple-oss-distributions/xnu @ ff8ca1ea.
+   bsd/sys/pipe.h:77       -- `#define PIPE_KVAMAX (1024 * 1024 * 16)`
+   */
+#define XNU_PIPE_KVAMAX  (1024*1024*16)
+
+#define PIPES_NEEDED  ((XNU_PIPE_KVAMAX / XNU_PIPESIZE_BLOCKS_MAX) + 1)
+#define FDS_NEEDED    (PIPES_NEEDED * 2)
+
+int
+set_nonblock (int fd)
+{
+  int fl;
+  fl = fcntl (fd, F_GETFL, 0);
+  if (fl < 0)
+  {
+    return fl;
+  }
+
+  return fcntl (fd, F_SETFL, fl | O_NONBLOCK);
+}
+
+int
+main (int argc, char **argv)
+{
+  char* buf;
+  int* fds;
+  size_t nw;
+  size_t nw_total;
+  ssize_t remains;
+  int npipes, nfds;
+  int pipefds[2];
+
+  buf = calloc (XNU_PIPESIZE_BLOCKS_MAX, 1);
+  if (buf == NULL)
+  {
+    printf ("error allocating buffer: %s\n", strerror (errno));
+    exit (1);
+  }
+
+  fds = calloc (FDS_NEEDED, sizeof(int));
+  if (fds == NULL)
+  {
+    printf ("error allocating buffer: %s\n", strerror (errno));
+    exit (1);
+  }
+
+  printf ("creating %d %d-byte pipes...\n",
+    PIPES_NEEDED, XNU_PIPESIZE_BLOCKS_MAX);
+
+  for (npipes = 0; npipes < PIPES_NEEDED; npipes++)
+  {
+    if (pipe (pipefds) < 0)
+    {
+      printf ("could not create more pipes: %s\n", strerror (errno));
+      printf ("try increasing descriptor limit via ulimit\n");
+      exit (1);
+    }
+
+    fds[nfds] = pipefds[0];
+    fds[nfds+1] = pipefds[1];
+    nfds += 2;
+
+    if (set_nonblock(pipefds[1]) < 0)
+    {
+      printf ("error setting pipe as nonblocking: %s\n", strerror (errno));
+      exit (1);
+    }
+
+    /* If amountpipekva < maxpipekva, the full 64K will be written. */
+    nw = write (pipefds[1], buf, XNU_PIPESIZE_BLOCKS_MAX);
+    if (nw == XNU_PIPESIZE_BLOCKS_MAX)
+    {
+      nw_total += nw;
+      continue;
+    }
+
+    /* If not, we have pushed amountpipekva past maxpipekva. */
+    printf ("filled with %ld bytes (%d pipes)\n", nw_total, npipes);
+
+    /* Other processes closing pipes can cause amountpipekva to go back
+       below maxpipekva. Keep trying to fill it until this process is
+       intentionally interrupted by the user. */
+    ssize_t remains = XNU_PIPESIZE_BLOCKS_MAX - nw;
+    while (remains > 0)
+    {
+      nw = write (pipefds[1], buf, remains);
+      if (nw >= 0)
+      {
+        remains -= nw;
+      }
+    }
+  }
+
+  exit (0);
+}
+
+
+#else /* ifdef MACOSX */
+
+int main()
+{
+  /* This is only relevant to the XNU kernel's behavior. */
+  return 0;
+}
+
+#endif /* ifdef MACOSX */
diff --git a/tests/heredoc.right b/tests/heredoc.right
index 214ef520..9935265e 100644
--- a/tests/heredoc.right
+++ b/tests/heredoc.right
@@ -161,8 +161,16 @@ here-doc line 1
 here-doc line 2
 here-document
 here-document
+checking 512-byte heredoc for hang
+checking 1024-byte heredoc for hang
+checking 2048-byte heredoc for hang
+checking 4096-byte heredoc for hang
+checking 8192-byte heredoc for hang
+checking 16384-byte heredoc for hang
+checking 65536-byte heredoc for hang
+checking 65537-byte heredoc for hang
 comsub here-string
-./heredoc.tests: line 184: warning: here-document at line 181 delimited by end-of-file (wanted `')
+./heredoc.tests: line 187: warning: here-document at line 184 delimited by end-of-file (wanted `')
 hi
 there
 ''
diff --git a/tests/heredoc.tests b/tests/heredoc.tests
index d6431b8a..ef0a7a32 100644
--- a/tests/heredoc.tests
+++ b/tests/heredoc.tests
@@ -171,6 +171,9 @@ ${THIS_SH} ./heredoc9.sub
 # test various combinations of here-documents and aliases
 ${THIS_SH} ./heredoc10.sub
 
+# regression test for bash's interaction with macOS dynamic pipe capacities
+${THIS_SH} ./heredoc11.sub
+
 echo $(
 	cat <<< "comsub here-string"
 )
diff --git a/tests/heredoc11.sub b/tests/heredoc11.sub
new file mode 100644
index 00000000..fba1c778
--- /dev/null
+++ b/tests/heredoc11.sub
@@ -0,0 +1,43 @@
+#   This program is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Test here documents to ensure they don't hang at various sizes.
+# This is a regression test for how bash interacts with macOS's dynamic
+# pipe capacities.
+
+# start background test helper to fill pipekva
+(fillpipekva >/dev/null) &
+fillpipekva_pid=$!
+sleep 1
+
+# start background subshell to fail the test after 5 seconds
+(
+    sleep 5
+    echo timeout
+    kill -PIPE $$ &>/dev/null
+    kill $fillpipekva_pid &>/dev/null
+) &
+timeout_pid=$!
+
+# Try writing at every pipesize block (and past it)
+# See bsd/kern/sys_pipe.c line 307 at commit ff8ca1ea
+# of apple-oss-distributions/xnu for the list of sizes.
+for size in 512 1024 2048 4096 8192 16384 65536 65537
+do
+    echo "checking ${size}-byte heredoc for hang"
+    cat >/dev/null <<<$(printf "%0$((size-1))d" 0)
+done
+
+# Cleanup
+kill $fillpipekva_pid &>/dev/null
+kill $timeout_pid &>/dev/null
-- 
2.49.0

