gh-159603: Use pthread_attr_get_np() on FreeBSD (#159604)

Use pthread_attr_get_np() on FreeBSD to get the stack size (and guard
size) of the current thread.

Check if pthread_attr_get_np() function and <pthread_np.h> header are
available in configure.

Update test_json.test_recursion. On FreeBSD 16 with a default stack size of
512 MB, the json C implementation no longer raises RecursionError in
test_highly_nested_objects_encoding(). Reduce also max_depth from 5,000 to
2,000 so test_highly_nested_objects_encoding() of the Python implementation
only takes 1.6 seconds, instead of 12 seconds.
diff --git a/Lib/test/test_json/test_recursion.py b/Lib/test/test_json/test_recursion.py
index 17c7afe..e7e0783 100644
--- a/Lib/test/test_json/test_recursion.py
+++ b/Lib/test/test_json/test_recursion.py
@@ -91,22 +91,35 @@ def test_highly_nested_objects_decoding(self):
     @support.skip_emscripten_stack_overflow()
     @support.requires_resource('cpu')
     def test_highly_nested_objects_encoding(self):
-        # See #12051
+        # See issue gh-56260
+
+        # With max_depth=2_000 on Linux, the Python implementation takes around
+        # 2 seconds to raise RecursionError, whereas the C implementation only
+        # takes around 160 ms.
+        max_depth = 2_000
         l, d = [], {}
-        for x in range(500_000):
+        for x in range(max_depth * 10):
             l, d = [l], {'k':d}
-        with self.assertRaises(RecursionError):
-            with support.infinite_recursion(5000):
+
+        # Don't use assertRaises(RecursionError): on FreeBSD 16 with a default
+        # stack size of 512 MiB, the C implementation doesn't raise
+        # RecursionError.
+        try:
+            with support.infinite_recursion(max_depth):
                 self.dumps(l)
-        with self.assertRaises(RecursionError):
-            with support.infinite_recursion(5000):
+        except RecursionError:
+            pass
+        try:
+            with support.infinite_recursion(max_depth):
                 self.dumps(d)
+        except RecursionError:
+            pass
 
     @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_endless_recursion(self):
-        # See #12051
+        # See issue gh-56260
         class EndlessJSONEncoder(self.json.JSONEncoder):
             def default(self, o):
                 """If check_circular is False, this will keep adding another list."""
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-17-02-47-26.gh-issue-159603.OFagJz.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-17-02-47-26.gh-issue-159603.OFagJz.rst
new file mode 100644
index 0000000..bd46b85
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-17-02-47-26.gh-issue-159603.OFagJz.rst
@@ -0,0 +1,2 @@
+On FreeBSD, call ``pthread_attr_get_np()`` to get the stack size (and guard
+size) of the current thread. Patch by Victor Stinner.
diff --git a/Python/ceval.c b/Python/ceval.c
index a6a21f0..d63dffd 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -3,6 +3,11 @@
 #include "ceval.h"
 #include "pycore_long.h"
 
+#ifdef HAVE_PTHREAD_NP_H
+#  include <pthread_np.h>         // pthread_attr_get_np()
+#endif
+
+
 int
 Py_GetRecursionLimit(void)
 {
@@ -147,13 +152,18 @@ hardware_stack_limits(uintptr_t *base, uintptr_t *top, uintptr_t sp)
     // limits compared to the old stack size estimation. musl doesn't give
     // us a way to detect it, so we have to assume any non-glibc,
     // non-Android Linux is musl and mustn't rely on pthread_getattr_np.
-#  if defined(HAVE_PTHREAD_GETATTR_NP) && !defined(_AIX) && \
-        !defined(__NetBSD__) && \
-        (defined(__GLIBC__) || defined(__ANDROID__) || !defined(__linux__))
+#  if (defined(HAVE_PTHREAD_GETATTR_NP) || defined(_Py_HAVE_PTHREAD_ATTR_GET_NP)) \
+        && !defined(_AIX) && !defined(__NetBSD__) \
+        && (defined(__GLIBC__) || defined(__ANDROID__) || !defined(__linux__))
     size_t stack_size, guard_size;
     void *stack_addr;
     pthread_attr_t attr;
+#ifdef _Py_HAVE_PTHREAD_ATTR_GET_NP
+    int err = pthread_attr_init(&attr);
+    err |= pthread_attr_get_np(pthread_self(), &attr);
+#else
     int err = pthread_getattr_np(pthread_self(), &attr);
+#endif
     if (err == 0) {
         err = pthread_attr_getguardsize(&attr, &guard_size);
         err |= pthread_attr_getstack(&attr, &stack_addr, &stack_size);
diff --git a/Tools/build/check_capi_macros_ignored.txt b/Tools/build/check_capi_macros_ignored.txt
index 238c29b..008eba4 100644
--- a/Tools/build/check_capi_macros_ignored.txt
+++ b/Tools/build/check_capi_macros_ignored.txt
@@ -433,6 +433,7 @@
 HAVE_PTHREAD_H
 HAVE_PTHREAD_INIT
 HAVE_PTHREAD_KILL
+HAVE_PTHREAD_NP_H
 HAVE_PTHREAD_SETNAME_NP
 HAVE_PTHREAD_SET_NAME_NP
 HAVE_PTHREAD_SIGMASK
diff --git a/configure b/configure
index 217d8e1..72948d6 100755
--- a/configure
+++ b/configure
@@ -12074,6 +12074,12 @@
   printf "%s\n" "#define HAVE_PTHREAD_H 1" >>confdefs.h
 
 fi
+ac_fn_c_check_header_compile "$LINENO" "pthread_np.h" "ac_cv_header_pthread_np_h" "$ac_includes_default"
+if test "x$ac_cv_header_pthread_np_h" = xyes
+then :
+  printf "%s\n" "#define HAVE_PTHREAD_NP_H 1" >>confdefs.h
+
+fi
 ac_fn_c_check_header_compile "$LINENO" "pty.h" "ac_cv_header_pty_h" "$ac_includes_default"
 if test "x$ac_cv_header_pty_h" = xyes
 then :
@@ -21814,6 +21820,49 @@
 
 
 
+# FreeBSD pthread_attr_get_np()
+
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for pthread_attr_get_np" >&5
+printf %s "checking for pthread_attr_get_np... " >&6; }
+if test ${ac_cv_func_pthread_attr_get_np+y}
+then :
+  printf %s "(cached) " >&6
+else case e in #(
+  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <pthread_np.h>
+int
+main (void)
+{
+void *x=pthread_attr_get_np
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+  ac_cv_func_pthread_attr_get_np=yes
+else case e in #(
+  e) ac_cv_func_pthread_attr_get_np=no ;;
+esac
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+   ;;
+esac
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_func_pthread_attr_get_np" >&5
+printf "%s\n" "$ac_cv_func_pthread_attr_get_np" >&6; }
+  if test "x$ac_cv_func_pthread_attr_get_np" = xyes
+then :
+
+printf "%s\n" "#define _Py_HAVE_PTHREAD_ATTR_GET_NP 1" >>confdefs.h
+
+fi
+
+
+
+
 
 
 # os.statx uses Linux's statx function.  AIX also has a function named statx,
diff --git a/configure.ac b/configure.ac
index ca804d6..b8f5e2b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -3195,7 +3195,8 @@
   alloca.h asm/types.h bluetooth.h conio.h direct.h dlfcn.h endian.h errno.h fcntl.h grp.h \
   io.h langinfo.h libintl.h libutil.h linux/auxvec.h sys/auxv.h linux/fs.h linux/limits.h linux/memfd.h \
   linux/netfilter_ipv4.h linux/random.h linux/soundcard.h linux/sched.h \
-  linux/tipc.h linux/wait.h netdb.h net/ethernet.h netinet/in.h netpacket/packet.h poll.h process.h pthread.h pty.h \
+  linux/tipc.h linux/wait.h netdb.h net/ethernet.h netinet/in.h netpacket/packet.h \
+  poll.h process.h pthread.h pthread_np.h pty.h \
   sched.h setjmp.h shadow.h signal.h spawn.h sys/audioio.h sys/bsdtty.h sys/devpoll.h \
   sys/endian.h sys/epoll.h sys/event.h sys/eventfd.h sys/file.h sys/ioctl.h sys/kern_control.h \
   sys/loadavg.h sys/lock.h sys/memfd.h sys/mkdev.h sys/mman.h sys/modem.h sys/param.h sys/pidfd.h sys/poll.h \
@@ -5586,6 +5587,8 @@
 
 PY_CHECK_FUNC_PRIVATE([freadlink], [@%:@include <unistd.h>])
 PY_CHECK_FUNC_PRIVATE([strerror_r], [@%:@include <string.h>])
+# FreeBSD pthread_attr_get_np()
+PY_CHECK_FUNC_PRIVATE([pthread_attr_get_np], [@%:@include <pthread_np.h>])
 
 
 # os.statx uses Linux's statx function.  AIX also has a function named statx,
diff --git a/pyconfig.h.in b/pyconfig.h.in
index 65e235c..0c8c798 100644
--- a/pyconfig.h.in
+++ b/pyconfig.h.in
@@ -1138,6 +1138,9 @@
 /* Define to 1 if you have the 'pthread_kill' function. */
 #undef HAVE_PTHREAD_KILL
 
+/* Define to 1 if you have the <pthread_np.h> header file. */
+#undef HAVE_PTHREAD_NP_H
+
 /* Define to 1 if you have the 'pthread_setname_np' function. */
 #undef HAVE_PTHREAD_SETNAME_NP
 
@@ -2214,6 +2217,9 @@
 /* Define if you have the 'PR_SET_VMA_ANON_NAME' constant. */
 #undef _Py_HAVE_PR_SET_VMA_ANON_NAME
 
+/* Define if you have the 'pthread_attr_get_np' function. */
+#undef _Py_HAVE_PTHREAD_ATTR_GET_NP
+
 /* Define if you have the 'sinpi' function. */
 #undef _Py_HAVE_SINPI