[2/2] gdb: support zero inode in generate-core-file command

Message ID 0d389b435cbb0924335adbc9eba6cf30b4a2c4ee.1741776651.git.aburgess@redhat.com
State New
Headers
Series include shared memory with id zero in core NT_FILE note |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_gdb_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_gdb_check--master-aarch64 success Test passed
linaro-tcwg-bot/tcwg_gdb_check--master-arm success Test passed

Commit Message

Andrew Burgess March 12, 2025, 10:52 a.m. UTC
  It is possible, when creating a shared memory segment (i.e. with
shmget), that the id of the segment will be zero.

When looking at the segment in /proc/PID/smaps, the inode field of the
entry holds the shared memory segment id.

And so, it can be the case that an entry (in the smaps file) will have
an inode of zero.

When GDB generates a core file, with the generate-core-file (or its
gcore alias) command, the shared memory segment should be written into
the core file.

Fedora GDB has, since 2008, carried a patch that tests this case.
There is no fix for GDB associated with the test, and unfortunately,
the motivation for the test has been lost to the mists of time.  This
likely means that a fix was merged upstream without a suitable test,
but I've not been able to find and relevant commit.  The test seems to
be checking that the shared memory segment with id zero, is being
written to the core file.

While looking at this test and trying to work out if it should be
posted upstream, I saw that GDB does appear to write the shared memory
segment into the core file (as expected), which is good.  However, GDB
still isn't getting this case exactly right.

In gcore_memory_sections (gcore.c) we call back into linux-tdep.c (via
the gdbarch_find_memory_regions call) to correctly write the shared
memory segment into the core file, however, in
linux_make_mappings_corefile_notes, when we use
linux_find_memory_regions_full to create the NT_FILE note, we call
back into linux_make_mappings_callback for each mapping, and in here
we reject any mapping with a zero inode.

The result of this, is that, for a shared memory segment with a
non-zero id, after loading the core file, the shared memory segment
will appear in the 'proc info mappings' output.  But, for a shared
memory segment with a zero id, the segment will not appear in the
'proc info mappings' output.

I propose fixing this by not checking the inode in
linux_make_mappings_callback.  The inode check was in place since the
code was originally added in commit 451b7c33cb3c9ec6272c36870 (in
2012).

The test for this bug (a modified version of the original Fedora test)
is a little sketchy.  User space doesn't have any control over the
shared memory id, so all we can do is spam out requests for new shared
memory segments and hope that we eventually get the zero id.

Obviously, this can fail; the zero id might already be in use by some
long running process, or the kernel, for whatever reason, might choose
to never allocate the zero id.  But, for me, the test actually turns
out to be pretty reliable.  But that's just my machine, running my
work load.

I believe the fix in linux-tdep.c is sane, and should be merged.  But
we might decide that the test is just too unstable.  If this is the
case, then I propose that we merge the test, along with the fix, but
then immediately follow up with a commit to delete the test.  This
way, if, in the future, someone looks back through the git history at
this commit, they will be able to recover the test.  You never know
when information like this could be useful.

I've listed the author of the original test as co-author below.  But I
have made significant changes to the test, any mistakes should be
considered mine.

Co-Author: Jan Kratochvil <jkratoch@fedoraproject.org>
---
 gdb/linux-tdep.c                        |   2 +-
 gdb/testsuite/gdb.base/gcore-shmid0.c   | 183 ++++++++++++++++++++++++
 gdb/testsuite/gdb.base/gcore-shmid0.exp | 101 +++++++++++++
 3 files changed, 285 insertions(+), 1 deletion(-)
 create mode 100644 gdb/testsuite/gdb.base/gcore-shmid0.c
 create mode 100644 gdb/testsuite/gdb.base/gcore-shmid0.exp
  

Patch

diff --git a/gdb/linux-tdep.c b/gdb/linux-tdep.c
index d3ab02d03e0..e2d41e7964e 100644
--- a/gdb/linux-tdep.c
+++ b/gdb/linux-tdep.c
@@ -1712,7 +1712,7 @@  linux_make_mappings_callback (ULONGEST vaddr, ULONGEST size,
     = (struct linux_make_mappings_data *) data;
   gdb_byte buf[sizeof (ULONGEST)];
 
-  if (*filename == '\0' || inode == 0)
+  if (*filename == '\0')
     return 0;
 
   ++map_data->file_count;
diff --git a/gdb/testsuite/gdb.base/gcore-shmid0.c b/gdb/testsuite/gdb.base/gcore-shmid0.c
new file mode 100644
index 00000000000..01ebc46499c
--- /dev/null
+++ b/gdb/testsuite/gdb.base/gcore-shmid0.c
@@ -0,0 +1,183 @@ 
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2007-2025 Free Software Foundation, Inc.
+
+   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 GDB's core file creation for a shared memory mapping that has a
+   name, but a zero id.  The shared memory id is represented as the inode
+   in the file mappings list, and at one point GDB would ignore any mapping
+   with a zero inode.  */
+
+#include <sys/ipc.h>
+#include <sys/shm.h>
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <assert.h>
+#include <time.h>
+#include <pthread.h>
+
+/* This test relies on acquiring a shared memory mapping with the zero id.
+   Which id is assigned to a mapping is not under our control, so we just
+   need to keep asking the kernel for new mappings until we get a mapping
+   with the desired (zero) id.
+
+   Even then, if some other process, e.g. another instance of this test in
+   a parallel testsuite run, has already acquired the zero id, then this
+   test will never get the zero id.
+
+   All we can do is run for some period of time and hope that we eventually
+   get the desired id.  If we don't then we'll report this test as
+   unsupported.  */
+
+#define TIMEOUT_SEC 15
+
+/* Use multiple threads to increase our chance of creating the shared
+   memory mapping with the zero id.  */
+
+#define N_THREADS 4
+
+/* A global used to create filler work.  */
+static volatile int global_var;
+
+/* Setting this true causes all threads to terminate.  Shouldn't really use
+   volatile for thread synchronisation, but the only other state that
+   propagates from the worker threads to the main is the shared memory
+   mapping, so we're probably OK with volatile.  */
+static volatile int stop_all_threads_p = 0;
+
+static void
+initialized (void)
+{
+  /* Just some filler work.  */
+  global_var++;
+}
+
+static void
+unresolved (void)
+{
+  /* Just some filler work.  */
+  global_var++;
+}
+
+/* Thread worker function.  Spin creating and destroying shared memory
+   mappings, trying to acquire a mapping with id 0.  When the mapping with
+   id 0 is created, map the shared memory region into the process, and set
+   the global STOP_ALL_THREADS_P, then terminate this thread.
+
+   If some other thread sets STOP_ALL_THREADS_P then just terminate this
+   thread.  */
+
+static void *
+worker_function (void *arg)
+{
+  unsigned int *addr = (void *) -1L;
+
+  /* The generated SID will cycle with an increment of 32768, attempt until
+     it wraps to 0.  */
+  for (int attempt = 0; addr == (void *) -1L && !stop_all_threads_p; attempt++)
+    {
+      int sid = shmget (IPC_PRIVATE, 0x1000, IPC_CREAT | IPC_EXCL | 0777);
+      if (sid == -1)
+	{
+	  perror ("shmget");
+	  exit (1);
+	}
+
+      /* Use SID only if it is 0, retry it otherwise.  */
+      if (sid == 0)
+	{
+	  /* Attach the shared memory mapping.  */
+	  addr = shmat (sid, NULL, SHM_RND);
+	  if (addr == (void *) -1L)
+	    {
+	      perror ("shmat");
+	      exit (1);
+	    }
+
+	  /* Tell all the other threads to exit.  */
+	  stop_all_threads_p = 1;
+	}
+
+      /* Mark the shared memory mapping as deleted -- once the last user
+	 has finished with it.  */
+      if (shmctl (sid, IPC_RMID, NULL) != 0)
+	{
+	  perror ("shmctl");
+	  exit (1);
+	}
+    }
+}
+
+int
+main (void)
+{
+  pthread_t thr[N_THREADS];
+
+  /* Start all threads.  */
+  for (int i = 0; i < N_THREADS; ++i)
+    pthread_create (&thr[i], NULL, worker_function, NULL);
+
+  /* This is set to non-zero if we timeout.  */
+  int timeout_p = 0;
+
+  /* Used to track how long we've been waiting for the worker threads.  */
+  time_t ts_start, ts;
+
+  if (time (&ts_start) == (time_t) -1)
+    {
+      perror ("time");
+      exit (1);
+    }
+
+  /* Wait for at least TIMEOUT_SEC seconds, or until the global
+     STOP_ALL_THREADS_P is set to true.  If we timeout then set
+     STOP_ALL_THREADS_P, which will cause all the worker threads to stop
+     themselves.  */
+  while (!stop_all_threads_p)
+    {
+      usleep (1000);
+
+      if (time (&ts) == (time_t) -1)
+	{
+	  perror ("time");
+	  exit (1);
+	}
+
+      if (ts >= ts_start + TIMEOUT_SEC)
+	{
+	  stop_all_threads_p = 1;
+	  timeout_p = 1;
+	  break;
+	}
+    }
+
+  /* Join all threads.  */
+  for (int i = 0; i < N_THREADS; ++i)
+    pthread_join (thr[i], NULL);
+
+  /* If we timed-out then there's nothing to be done.  */
+  if (timeout_p)
+    {
+      unresolved ();
+      exit (1);
+    }
+
+  /* Success!  We managed to create a shared memory mapping with id 0.  */
+  initialized ();
+
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/gcore-shmid0.exp b/gdb/testsuite/gdb.base/gcore-shmid0.exp
new file mode 100644
index 00000000000..932717e86bb
--- /dev/null
+++ b/gdb/testsuite/gdb.base/gcore-shmid0.exp
@@ -0,0 +1,101 @@ 
+# Copyright 2007-2025 Free Software Foundation, Inc.
+#
+# 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 GDB's handling of gcore for shared memory mappings with a name
+# but zero inode.  The inode of a shared memory mapping is actually
+# its shared memory id, which can be zero.
+
+require gcore_cmd_available
+require {istarget *-linux*}
+require {!is_remote host}
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" $testfile $srcfile \
+	  {debug pthreads}] == -1 } {
+    return
+}
+
+if { ![ runto_main ] } {
+    return
+}
+
+gdb_breakpoint "initialized"
+gdb_breakpoint "unresolved"
+
+with_timeout_factor 3 {
+    gdb_test_multiple "continue" "setup shmid 0 test" {
+	-re -wrap "Breakpoint .*, initialized .* at .*" {
+	    pass $gdb_test_name
+	}
+	-re -wrap "Breakpoint .*, unresolved .* at .*" {
+	    unsupported $gdb_test_name
+	    return
+	}
+    }
+}
+
+# Ensure that the shared memory region shows up in 'proc mappings'.
+set shm_start ""
+set shm_end ""
+gdb_test_multiple "info proc mappings" "" {
+    -re "^($hex)\\s+($hex)\\s+\[^\r\n\]+/SYSV0+ \\(deleted\\)\\s*\r\n" {
+	set shm_start $expect_out(1,string)
+	set shm_end $expect_out(2,string)
+	exp_continue
+    }
+
+    -re "^$gdb_prompt $" {
+	gdb_assert { $shm_start ne "" && $shm_end ne "" } $gdb_test_name
+    }
+
+    -re "^\[^\r\n\]*\r\n" {
+	exp_continue
+    }
+}
+
+# Create the core file.
+set corefile_name [standard_output_file gcore-shmid0.core]
+if { ![gdb_gcore_cmd $corefile_name "create core file"] } {
+    return
+}
+
+# Allow the inferior to finish.  This ensures that the shared memory
+# region is cleaned up correctly.  That said, the shared memory region
+# should already be marked as deleted by this point, so it should be
+# OK to just kill the inferior.  But letting the inferior finish
+# doesn't hurt.
+gdb_continue_to_end "finish"
+
+# Restart GDB.
+clean_restart $binfile
+
+# Load the core file.
+if { [gdb_core_cmd $corefile_name "load core file"] != 1 } {
+    return
+}
+
+gdb_test "bt" \
+    [multi_line \
+	 "#0\\s+initialized \\(\\) at \[^\r\n\]+" \
+	 "#1\\s+$hex in main \\(\\) at \[^\r\n\]+"]
+
+gdb_test "info proc mappings" "/SYSV0+ \\(deleted\\).*" \
+    "shared mapping shows after core file load"
+
+gdb_test "x/8w $shm_start" \
+    [multi_line \
+	 "$hex:\\s+0\\s+0\\s+0\\s+0\\s*" \
+	 "$hex:\\s+0\\s+0\\s+0\\s+0\\s*"]