Compare commits

...

7 Commits

Author SHA1 Message Date
Andrew Burgess
5e93fc16e0 gdb: support zero inode in generate-core-file command
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, there appears to be no
NT_FILE entry for the shared memory mapping if the mapping had an id
of zero.

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 in to dump_note_entry_p 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 initially tried just dropping the inode check in this function (see
previous commit 1e21c846c2, which I then reverted in commit
998165ba99.

The problem with dropping the inode check is that the special kernel
mappings, e.g. '[vvar]' would now get a NT_FILE entry.  In fact, any
special entry except '[vdso]' and '[vsyscall]' which are specifically
checked for in dump_note_entry_p would get a NT_FILE entry, which is
not correct.

So, instead, I propose that if the inode is zero, and the filename
starts with '[' and finished with ']' then we should not create a
NT_FILE entry.  But otherwise a zero inode should not prevent a
NT_FILE entry being created.

The test for this change is a bit tricky.  The original Fedora
test (mentioned above) has a loop that tries to grab the shared memory
mapping with id zero.  This was, unfortunately, not very reliable.

I tried to make this more reliable by going multi-threaded, and
waiting for longer, see my proposal here:

  https://inbox.sourceware.org/gdb-patches/0d389b435cbb0924335adbc9eba6cf30b4a2c4ee.1741776651.git.aburgess@redhat.com

But this was still not great.  On further testing this was only
passing (i.e. managing to find the shared memory mapping with id zero)
about 60% of the time.

However, I realised that GDB finds the shared memory id by reading the
/proc/PID/smaps file.  But we don't really _need_ the shared memory id
for anything, we just use the value (as an inode) to decide if the
segment should be included in the core file or not.  The id isn't even
written to the core file.  So, if we could intercept the read of the
smaps file, then maybe, we could lie to GDB, and tell it that the id
was zero, and then see how GDB handles this.

And luckily, we can do that using a preload library!

We already have a test that uses a preload library to modify GDB, see
gdb.threads/attach-slow-waitpid.exp.

So, I have created a new preload library.  This one intercepts open,
close, read, and pread.  When GDB attempts to open /proc/PID/smaps,
the library spots this and loads the file contents into a memory
buffer.  The buffer is then modified to change the id of any shared
memory mapping to zero.  Any reads from this file are served from the
modified memory buffer.

And so, the test is now simple.  Start GDB with the preload library in
place, start the inferior and generate a core file.  Then restart GDB,
load the core file, and check the shared memory mapping was included.
This test will fail with an unpatched GDB, and succeed with the patch
applied.
2025-05-12 14:29:09 +01:00
Andrew Burgess
7a3bbd74c8 gdb: pass std::string from linux_find_memory_regions_full
Update linux_find_memory_region_ftype to take 'const std::string &'
instead of 'const char *', update the two functions which are passed
as callbacks to linux_find_memory_regions_full.

There should be no user visible changes after this commit.
2025-05-07 20:33:17 +01:00
Andrew Burgess
add5cd03c6 gdb: remove unnecessary function declaration
There's no need to declare a function immediately before its
definition.  Lets not do that.

There should be no user visible changes after this commit.
2025-05-07 20:33:07 +01:00
Andrew Burgess
d96d2aec84 gdb: move extra checks into dump_note_entry_p
Now that dump_note_entry_p is always called (see previous commit), we
can move some of the checks out of linux_make_mappings_callback into
dump_note_entry_p.

The checks only exist in linux_make_mappings_callback because, before
the previous commit, we couldn't be sure that dump_note_entry_p would
be called or not, so linux_make_mappings_callback had to run its own
checks.

Now that dump_note_entry_p is always called we can rely on that
function to filter out which mappings should result in an NT_FILE
entry, and linux_make_mappings_callback can just create an entry for
everything it is passed.

As a result of this change I was able to remove the inode argument
from linux_make_mappings_callback and
linux_find_memory_regions_thunk.  The inode check has now moved to
dump_note_entry_p.

There should be no user visible changes after this commit.
2025-05-07 20:29:35 +01:00
Andrew Burgess
f6957916d5 gdb: always call should_dump_mapping_p during core file creation
This commit moves the logic for whether should_dump_mapping_p is
called out of linux_find_memory_regions_full and pushes it down into
the two callback functions that are used as the should_dump_mapping_p
callback; `dump_mapping_p` and `dump_note_entry_p`.

Older Linux kernels don't make the 'Anonymous' information available
in the smaps file, and currently, GDB handles this by not calling the
should_dump_mapping_p callback in linux_find_memory_regions_full,
instead the answer is hard-coded to true.

This is (maybe) fine for dump_mapping_p, but for dump_note_entry_p,
this choice makes little sense.  The dump_note_entry_p function
doesn't even use the anonymous mapping information.

I propose that the 'has_anonymous' check should be moved out of
linux_find_memory_regions_full, and pushed into dump_mapping_p.  Then
in dump_note_entry_p there will be no has_anonymous check; it just
isn't needed.

This allows linux_find_memory_regions_full to be simplified a little,
and will allow some additional clean ups in
linux_make_mappings_callback, which is the partner function to
dump_note_entry_p (see linux_make_mappings_corefile_notes), now that
we know dump_note_entry_p is always called.  This follow on clean up
will be done in a later commit in this series.

Looking at dump_mapping_p, I do wonder if the ::has_anonymous check
could be moved later in the function.  The first few checks in
dump_mapping_p don't rely on the anonymous information, so running
them might give better results.  However, the lack of the anonymous
information is only for older kernels, so testing any changes in this
area would likely require spinning up an older kernel, and as the
years pass, we likely care about this case less.  So for now I've left
the ::has_anonymous check as the first thing in dump_mapping_p as this
keeps the existing behaviour.

There should be no user visible changes after this commit.
2025-05-07 20:20:42 +01:00
Andrew Burgess
bb71410f3a gdb: pass struct smaps_data to linux_dump_mapping_p_ftype
Simplify the argument passing in linux_find_memory_regions_full when
calling the should_dump_mapping_p callback.  Instead of pulling all
the components from the smaps_data object and passing them separately,
just pass the smaps_data object.

I think this change is justified on its own; the code seems cleaner,
and easier to read to my eye.  But additionally, in a later commit in
this series I want to pass smaps_data::has_anonymous to the
should_dump_mapping_p callback, which would mean adding yet another
argument, and I think the argument list is already long enough.
Changing the function now to pass the smaps_data object means that I
will already have the ::has_anonymous field available in the later
commit.

There should be no user visible changes after this commit.
2025-05-07 20:17:04 +01:00
Andrew Burgess
a73fbd7eba gdb: use bool more in linux-tdep.c
Convert linux_dump_mapping_p_ftype to return a bool, and then update
everything that is needed to handle the fallout from this change.

There should be no user visible changes from this commit.
2025-05-07 10:09:43 +01:00
4 changed files with 787 additions and 76 deletions

View File

@@ -630,9 +630,9 @@ mapping_is_anonymous_p (const char *filename)
return 0;
}
/* Return 0 if the memory mapping (which is related to FILTERFLAGS, V,
MAYBE_PRIVATE_P, MAPPING_ANONYMOUS_P, ADDR and OFFSET) should not
be dumped, or greater than 0 if it should.
/* Return 0 if the memory mapping represented by MAP should not be dumped,
or greater than 0 if it should. FILTERFLAGS guides which mappings
should be dumped.
In a nutshell, this is the logic that we follow in order to decide
if a mapping should be dumped or not.
@@ -677,11 +677,14 @@ mapping_is_anonymous_p (const char *filename)
header (of a DSO or an executable, for example). If it is, and
if the user is interested in dump it, then we should dump it. */
static int
dump_mapping_p (filter_flags filterflags, const struct smaps_vmflags *v,
int maybe_private_p, int mapping_anon_p, int mapping_file_p,
const char *filename, ULONGEST addr, ULONGEST offset)
static bool
dump_mapping_p (filter_flags filterflags, const smaps_data &map)
{
/* Older Linux kernels did not support the "Anonymous:" counter.
If it is missing, we can't be sure what to dump, so dump everything. */
if (!map.has_anonymous)
return true;
/* Initially, we trust in what we received from our caller. This
value may not be very precise (i.e., it was probably gathered
from the permission line in the /proc/PID/smaps list, which
@@ -689,41 +692,42 @@ dump_mapping_p (filter_flags filterflags, const struct smaps_vmflags *v,
what we have until we take a look at the "VmFlags:" field
(assuming that the version of the Linux kernel being used
supports it, of course). */
int private_p = maybe_private_p;
int dump_p;
int private_p = map.priv;
/* We always dump vDSO and vsyscall mappings, because it's likely that
there'll be no file to read the contents from at core load time.
The kernel does the same. */
if (strcmp ("[vdso]", filename) == 0
|| strcmp ("[vsyscall]", filename) == 0)
return 1;
if (map.filename == "[vdso]" || map.filename == "[vsyscall]")
return true;
if (v->initialized_p)
if (map.vmflags.initialized_p)
{
/* We never dump I/O mappings. */
if (v->io_page)
return 0;
if (map.vmflags.io_page)
return false;
/* Check if we should exclude this mapping. */
if (!dump_excluded_mappings && v->exclude_coredump)
return 0;
if (!dump_excluded_mappings && map.vmflags.exclude_coredump)
return false;
/* Update our notion of whether this mapping is shared or
private based on a trustworthy value. */
private_p = !v->shared_mapping;
private_p = !map.vmflags.shared_mapping;
/* HugeTLB checking. */
if (v->uses_huge_tlb)
if (map.vmflags.uses_huge_tlb)
{
if ((private_p && (filterflags & COREFILTER_HUGETLB_PRIVATE))
|| (!private_p && (filterflags & COREFILTER_HUGETLB_SHARED)))
return 1;
return true;
return 0;
return false;
}
}
int mapping_anon_p = map.mapping_anon_p;
int mapping_file_p = map.mapping_file_p;
bool dump_p;
if (private_p)
{
if (mapping_anon_p && mapping_file_p)
@@ -763,7 +767,7 @@ dump_mapping_p (filter_flags filterflags, const struct smaps_vmflags *v,
A mapping contains an ELF header if it is a private mapping, its
offset is zero, and its first word is ELFMAG. */
if (!dump_p && private_p && offset == 0
if (!dump_p && private_p && map.offset == 0
&& (filterflags & COREFILTER_ELF_HEADERS) != 0)
{
/* Useful define specifying the size of the ELF magical
@@ -774,7 +778,7 @@ dump_mapping_p (filter_flags filterflags, const struct smaps_vmflags *v,
/* Let's check if we have an ELF header. */
gdb_byte h[SELFMAG];
if (target_read_memory (addr, h, SELFMAG) == 0)
if (target_read_memory (map.start_address, h, SELFMAG) == 0)
{
/* The EI_MAG* and ELFMAG* constants come from
<elf/common.h>. */
@@ -783,7 +787,7 @@ dump_mapping_p (filter_flags filterflags, const struct smaps_vmflags *v,
{
/* This mapping contains an ELF header, so we
should dump it. */
dump_p = 1;
dump_p = true;
}
}
}
@@ -794,20 +798,24 @@ dump_mapping_p (filter_flags filterflags, const struct smaps_vmflags *v,
/* As above, but return true only when we should dump the NT_FILE
entry. */
static int
dump_note_entry_p (filter_flags filterflags, const struct smaps_vmflags *v,
int maybe_private_p, int mapping_anon_p, int mapping_file_p,
const char *filename, ULONGEST addr, ULONGEST offset)
static bool
dump_note_entry_p (filter_flags filterflags, const smaps_data &map)
{
/* vDSO and vsyscall mappings will end up in the core file. Don't
put them in the NT_FILE note. */
if (strcmp ("[vdso]", filename) == 0
|| strcmp ("[vsyscall]", filename) == 0)
return 0;
/* No NT_FILE entry for mappings with no filename. */
if (map.filename.length () == 0)
return false;
/* Special kernel mappings, those with names like '[vdso]' and
'[vsyscall]' will be placed in the core file, but shouldn't get an
NT_FILE entry. These special mappings all have a zero inode. */
if (map.inode == 0
&& map.filename.front () == '['
&& map.filename.back () == ']')
return false;
/* Otherwise, any other file-based mapping should be placed in the
note. */
return 1;
return true;
}
/* Implement the "info proc" command. */
@@ -1314,21 +1322,15 @@ linux_core_xfer_siginfo (struct gdbarch *gdbarch, gdb_byte *readbuf,
}
typedef int linux_find_memory_region_ftype (ULONGEST vaddr, ULONGEST size,
ULONGEST offset, ULONGEST inode,
ULONGEST offset,
int read, int write,
int exec, int modified,
bool memory_tagged,
const char *filename,
const std::string &filename,
void *data);
typedef int linux_dump_mapping_p_ftype (filter_flags filterflags,
const struct smaps_vmflags *v,
int maybe_private_p,
int mapping_anon_p,
int mapping_file_p,
const char *filename,
ULONGEST addr,
ULONGEST offset);
typedef bool linux_dump_mapping_p_ftype (filter_flags filterflags,
const struct smaps_data &map);
/* Helper function to parse the contents of /proc/<pid>/smaps into a data
structure, for easy access.
@@ -1590,35 +1592,15 @@ linux_find_memory_regions_full (struct gdbarch *gdbarch,
for (const struct smaps_data &map : smaps)
{
int should_dump_p = 0;
if (map.has_anonymous)
{
should_dump_p
= should_dump_mapping_p (filterflags, &map.vmflags,
map.priv,
map.mapping_anon_p,
map.mapping_file_p,
map.filename.c_str (),
map.start_address,
map.offset);
}
else
{
/* Older Linux kernels did not support the "Anonymous:" counter.
If it is missing, we can't be sure - dump all the pages. */
should_dump_p = 1;
}
/* Invoke the callback function to create the corefile segment. */
if (should_dump_p)
if (should_dump_mapping_p (filterflags, map))
{
func (map.start_address, map.end_address - map.start_address,
map.offset, map.inode, map.read, map.write, map.exec,
map.offset, map.read, map.write, map.exec,
1, /* MODIFIED is true because we want to dump
the mapping. */
map.vmflags.memory_tagging != 0,
map.filename.c_str (), obfd);
map.filename, obfd);
}
}
@@ -1644,10 +1626,10 @@ struct linux_find_memory_regions_data
static int
linux_find_memory_regions_thunk (ULONGEST vaddr, ULONGEST size,
ULONGEST offset, ULONGEST inode,
ULONGEST offset,
int read, int write, int exec, int modified,
bool memory_tagged,
const char *filename, void *arg)
const std::string &filename, void *arg)
{
struct linux_find_memory_regions_data *data
= (struct linux_find_memory_regions_data *) arg;
@@ -1693,8 +1675,6 @@ struct linux_make_mappings_data
struct type *long_type;
};
static linux_find_memory_region_ftype linux_make_mappings_callback;
/* A callback for linux_find_memory_regions_full that updates the
mappings data for linux_make_mappings_corefile_notes.
@@ -1703,17 +1683,16 @@ static linux_find_memory_region_ftype linux_make_mappings_callback;
static int
linux_make_mappings_callback (ULONGEST vaddr, ULONGEST size,
ULONGEST offset, ULONGEST inode,
ULONGEST offset,
int read, int write, int exec, int modified,
bool memory_tagged,
const char *filename, void *data)
const std::string &filename, void *data)
{
struct linux_make_mappings_data *map_data
= (struct linux_make_mappings_data *) data;
gdb_byte buf[sizeof (ULONGEST)];
if (*filename == '\0' || inode == 0)
return 0;
gdb_assert (filename.length () > 0);
++map_data->file_count;
@@ -1724,7 +1703,7 @@ linux_make_mappings_callback (ULONGEST vaddr, ULONGEST size,
pack_long (buf, map_data->long_type, offset);
obstack_grow (map_data->data_obstack, buf, map_data->long_type->length ());
obstack_grow_str0 (map_data->filename_obstack, filename);
obstack_grow_str0 (map_data->filename_obstack, filename.c_str ());
return 0;
}

View File

@@ -0,0 +1,441 @@
/* This testcase is part of GDB, the GNU debugger.
Copyright 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/>. */
/* This file contains a library that can be preloaded into GDB on Linux
using the LD_PRELOAD technique.
The library intercepts calls to OPEN, CLOSE, READ, and PREAD in order to
fake the inode number of a shared memory mapping.
When GDB creates a core file (e.g. with the 'gcore' command), then
shared memory mappings should be included in the generated core file.
The 'id' for the shared memory mapping shares the inode slot in the
/proc/PID/smaps file, which is what GDB consults to decide which
mappings should be included in the core file.
It is possible for a shared memory mapping to have an 'id' of zero.
At one point there was a bug in GDB where mappings with an inode of zero
would not be included in the generated core file. This meant that most
shared memory mappings would be included in the generated core file,
but, if a shared memory mapping happened to get an 'id' of zero, then,
because this would appear as a zero inode in the smaps file, this shared
memory mapping would be excluded from the generated core file.
This preload library spots when GDB opens a /proc/PID/smaps file and
immediately copies the contents of this file into an internal buffer.
The buffer is then scanned looking for a shared memory mapping, and, if
a shared memory mapping is found, its 'id' (in the inode position) is
changed to zero.
Calls to read/pread are intercepted, and attempts to read from the smaps
file are then served from the modified buffer contents.
The close calls are monitored and, when the smaps file is closed, the
internal buffer is released.
This works with GDB (currently) because the requirements for access to
the smaps file are pretty simple. GDB opens the file and grabs the
entire contents with a single pread call and a large buffer. There's no
seeking within the file or anything like that.
The intention is that this library is preloaded into a GDB session which
is then used to start an inferior and generate a core file. GDB will
then see the zero inode for the shared memory mapping and should, if the
bug is correctly fixed, still add the shared memory mapping to the
generated core file. */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdarg.h>
#include <errno.h>
#include <ctype.h>
#include <string.h>
#include <stdbool.h>
#include <assert.h>
/* Logging. */
static void
log_msg (const char *fmt, ...)
{
#ifdef LOGGING
va_list ap;
va_start (ap, fmt);
vfprintf (stderr, fmt, ap);
va_end (ap);
#endif /* LOGGING */
}
/* Error handling, message and exit. */
static void
error (const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
vfprintf (stderr, fmt, ap);
va_end (ap);
exit (EXIT_FAILURE);
}
/* The type of the open() function. */
typedef int (*open_func_type)(const char *pathname, int flags, ...);
/* The type of the close() function. */
typedef int (*close_func_type)(int fd);
/* The type of the read() function. */
typedef ssize_t (*read_func_type)(int fd, void *buf, size_t count);
/* The type of the pread() function. */
typedef ssize_t (*pread_func_type) (int fd, void *buf, size_t count, off_t offset);
/* Structure that holds information about a /proc/PID/smaps file that has
been opened. */
struct interesting_file
{
/* The file descriptor for the opened file. */
int fd;
/* The read offset within the file. Set to zero when the file is
opened. Any 'read' calls will update this offset. */
size_t offset;
/* The size of the contents within the buffer. This is not the total
buffer size (which might be larger). Attempts to read beyond SIZE
indicate an attempt to read beyond the end of the file. */
size_t size;
/* The (possibly modified) contents of the file. */
char *content;
};
/* We only track a single interesting file. Currently, for the use case
we imagine, GDB will only ever open one /proc/PID/smaps file at once. */
struct interesting_file the_file = { -1, 0, 0, NULL };
/* Update the contents of the global THE_FILE buffer. It is assumed that
the file contents have already been loaded into THE_FILE's content
buffer.
Look for any lines that represent a shared memory mapping and modify
the inode field (which holds the shared memory id) to be zero. */
static void
update_file_content_buffer (void)
{
assert (the_file.content != NULL);
char *start = the_file.content;
do
{
/* Every line, even the last one, ends with a newline. */
char *end = strchrnul (start, '\n');
assert (end != NULL);
assert (*end != '\0');
/* Attribute lines start with an uppercase letter. The lines we want
to modify should start with a lower case hex character,
i.e. [0-9a-f]. Also, every line that we want to consider should
be long enough, but just in case, check the longest possible
filename that we care about. */
if (isxdigit (*start) && (isdigit (*start) || islower (*start))
&& (end - start) > 23)
{
/* There are two possible filenames that we look for:
/SYSV%08x
/SYSV%08x (deleted)
The END pointer is pointing to the first character after the
filename.
Setup OFFSET to be the offset from END to the start of the
filename. As we check the filename we set OFFSET to 0 if the
filename doesn't match one of the expected patterns. */
size_t offset;
if (strncmp ((end - 13), "/SYSV", 5) == 0)
offset = 13;
else if (strncmp ((end - 23), "/SYSV", 5) == 0)
{
if (strncmp ((end - 10), " (deleted)", 10) == 0)
offset = 23;
else
offset = 0;
}
else
offset = 0;
for (int i = 0; i < 8 && offset != 0; ++i)
{
if (!isdigit (*(end - offset + 5 + i)))
offset = 0;
}
/* If OFFSET is non-zero then the filename on this line looks
like a shared memory mapping, and OFFSET is the offset from
END to the first character of the filename. */
if (offset != 0)
{
log_msg ("[LD_PRELOAD] shared memory entry: %.*s\n",
offset, (end - offset));
/* Set PTR to the first character before the filename. This
should be a white space character. */
char *ptr = end - offset - 1;
assert (isspace (*ptr));
/* Walk backwards until we find the inode field. */
while (isspace (*ptr))
--ptr;
/* Now replace every character in the inode field, except the
first one, with a space character. */
while (!isspace (*(ptr - 1)))
{
assert (isdigit (*ptr));
*ptr = ' ';
--ptr;
}
/* Replace the first character with '0'. */
assert (isdigit (*ptr));
*ptr = '0';
/* This print is checked for from GDB. */
printf ("[LD_PRELOAD] updated a shared memory mapping\n");
}
}
/* Update START to point to the next line. The last line of the
file will be empty. */
assert (*end == '\n');
start = end;
while (*start == '\n')
++start;
}
while (*start != '\0');
}
/* Intercept calls to 'open'. If this is an attempt to open a
/proc/PID/smaps file then intercept it, load the file contents into a
buffer and update the file contents. For all other open requests, just
forward to the real open function. */
int
open (const char *pathname, int flags, ...)
{
/* Pointer to the real open function. */
static open_func_type real_open = NULL;
/* Mode is only used if the O_CREAT flag is set in FLAGS. */
mode_t mode = 0;
/* Set true if this is a /proc/PID/smaps file. */
bool is_interesting = false;
/* Check if O_CREAT is in flags. If it is, get the mode. */
if (flags & O_CREAT) {
va_list args;
va_start (args, flags);
mode = va_arg (args, mode_t);
va_end (args);
}
/* Is this a '/proc/PID/smaps' filename? */
if (the_file.fd == -1 && strncmp (pathname, "/proc/", 6) == 0)
{
int idx = 6;
while (isdigit (pathname[idx]))
idx++;
if (idx > 6 && strcmp (&pathname[idx], "/smaps") == 0)
is_interesting = true;
}
/* Debug. */
if (is_interesting)
log_msg ("[LD_PRELOAD] Opening file: %s\n", pathname);
/* Make sure we have a pointer to the real open() function. */
if (real_open == NULL)
{
/* Get the address of the real open() function. */
real_open = (open_func_type) dlsym (RTLD_NEXT, "open");
if (real_open == NULL)
error ("[LD_PRELOAD] dlsym() error for 'open': %s\n", dlerror ());
}
/* Call the original open() function with the provided arguments. */
int res = -1;
if (flags & O_CREAT)
res = real_open (pathname, flags, mode);
else
res = real_open (pathname, flags);
if (is_interesting)
{
#define BLOCK_SIZE 1024
/* Slurp contents into a local buffer. */
size_t buffer_size = 1024;
size_t offset = 0;
assert (the_file.size == 0);
assert (the_file.content == NULL);
assert (the_file.fd == -1);
assert (the_file.offset == 0);
do
{
the_file.content = (char *) realloc (the_file.content, buffer_size);
if (the_file.content == NULL)
error ("[LD_PRELOAD] Failed allocating memory: %s\n", strerror (errno));
ssize_t bytes_read = read (res, the_file.content + offset, BLOCK_SIZE);
if (bytes_read == -1)
error ("[LD_PRELOAD] Failed reading file: %s\n", strerror (errno));
the_file.size += bytes_read;
if (bytes_read < BLOCK_SIZE)
break;
offset += BLOCK_SIZE;
buffer_size += BLOCK_SIZE;
}
while (true);
/* Add a null terminator. This makes the update easier. We know
there will be space because we only break out of the loop above
when the last read returns less than BLOCK_SIZE bytes. This means
we allocated an extra BLOCK_SIZE bytes, but didn't fill them all.
This means there must be at least 1 byte available for the null. */
the_file.content[the_file.size] = '\0';
/* Reset the seek pointer. */
if (lseek (res, 0, SEEK_SET) == (off_t) -1)
error ("[LD_PRELOAD] Failed to lseek in file: %s\n", strerror (errno));
/* Record the file descriptor, this is used in read, pread, and close
in order to spot when we need to intercept the call. */
the_file.fd = res;
update_file_content_buffer ();
#undef BLOCK_SIZE
}
return res;
}
/* Intercept the 'close' function. If this is a previously opened
interesting file then clean up. Otherwise, forward to the normal close
function. */
int
close (int fd)
{
static close_func_type real_close = NULL;
if (fd == the_file.fd)
{
the_file.fd = -1;
free (the_file.content);
the_file.content = NULL;
the_file.offset = 0;
the_file.size = 0;
log_msg ("[LD_PRELOAD] Closing file.\n");
}
/* Make sure we have a pointer to the real open() function. */
if (real_close == NULL)
{
/* Get the address of the real open() function. */
real_close = (close_func_type) dlsym (RTLD_NEXT, "close");
if (real_close == NULL)
error ("[LD_PRELOAD] dlsym() error for 'close': %s\n", dlerror ());
}
return real_close (fd);
}
/* Intercept 'pread' calls. If this is a pread from a previously opened
interesting file, then read from the in memory buffer. Otherwise,
forward to the real pread function. */
ssize_t
pread (int fd, void *buf, size_t count, off_t offset)
{
static pread_func_type real_pread = NULL;
if (fd == the_file.fd)
{
size_t max;
if (offset > the_file.size)
max = 0;
else
max = the_file.size - offset;
if (count > max)
count = max;
memcpy (buf, the_file.content + offset, count);
log_msg ("[LD_PRELOAD] Read from file.\n");
return count;
}
if (real_pread == NULL)
{
/* Get the address of the real read() function. */
real_pread = (pread_func_type) dlsym (RTLD_NEXT, "pread");
if (real_pread == NULL)
error ("[LD_PRELOAD] dlsym() error for 'pread': %s\n", dlerror ());
}
return real_pread (fd, buf, count, offset);
}
/* Intercept 'read' calls. If this is a read from a previously opened
interesting file, then read from the in memory buffer. Otherwise,
forward to the real read function. */
ssize_t
read (int fd, void *buf, size_t count)
{
static read_func_type real_read = NULL;
if (fd == the_file.fd)
{
ssize_t bytes_read = pread (fd, buf, count, the_file.offset);
if (bytes_read > 0)
the_file.offset += bytes_read;
return bytes_read;
}
if (real_read == NULL)
{
/* Get the address of the real read() function. */
real_read = (read_func_type) dlsym (RTLD_NEXT, "read");
if (real_read == NULL)
error ("[LD_PRELOAD] dlsym() error for 'read': %s\n", dlerror ());
}
return real_read (fd, buf, count);
}

View File

@@ -0,0 +1,63 @@
/* This testcase is part of GDB, the GNU debugger.
Copyright 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/>. */
#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>
void
breakpt (void)
{
/* Nothing. */
}
int
main (void)
{
/* Create a shared memory mapping. */
int sid = shmget (IPC_PRIVATE, 0x1000, IPC_CREAT | IPC_EXCL | 0777);
if (sid == -1)
{
perror ("shmget");
exit (1);
}
/* Attach the shared memory mapping. */
void *addr = shmat (sid, NULL, SHM_RND);
if (addr == (void *) -1L)
{
perror ("shmat");
exit (1);
}
breakpt ();
/* 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);
}
return 0;
}

View File

@@ -0,0 +1,228 @@
# Copyright 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/>.
# This test script tries to check GDB's ability to create a core file
# (e.g. with 'gcore' command) when there's a shared memory mapping
# with the id zero.
#
# Testing this case is hard. Older kernels don't even seem to give
# out the shared memory id zero. And on new kernels you still cannot
# guarantee to grab the zero id for testing; the id might be in use by
# some other process, or the kernel might just not give out that id
# for some other reason.
#
# To figure out which mappings to include in the core file, GDB reads
# the /proc/PID/smaps file. There is a field in this file which for
# file backed mappings, holds the inode of the file. But for shared
# memory mappings this field holds the shared memory id. The problem
# was that GDB would ignore any entry in /proc/PID/smaps with an inode
# entry of zero, which would catch the shared memory mapping with a
# zero id.
#
# There was an attempt to write a test which spammed out requests for
# shared memory mappings and tried to find the one with id zero, but
# this was still really unreliable.
#
# This test takes a different approach. We compile a library which we
# preload into the GDB process. This library intercepts calls to
# open, close, read, and pread, and watches for an attempt to open the
# /proc/PID/smaps file.
#
# When we see that file being opened, we copy the file contents into a
# memory buffer and modify the buffer so that the inode field for any
# shared memory mappings is set to zero. We then intercept calls to
# read and pread and return results from that in memory buffer.
#
# The test executable itself create a shared memory mapping (which
# might have any id).
#
# GDB, with the pre-load library in place, start the inferior and then
# uses the 'gcore' command to dump a core file. When GDB opens the
# smaps file and reads from it, the preload library ensures that GDB
# sees an inode of zero.
#
# This test only works on Linux
require isnative
require {!is_remote host}
require {!is_remote target}
require {istarget *-linux*}
require gcore_cmd_available
standard_testfile .c -lib.c
set libfile ${testfile}-lib
set libobj [standard_output_file ${libfile}.so]
# Compile the preload library. We only get away with this as we
# limit this test to running when ISNATIVE is true.
if { [build_executable "build preload lib" $libobj $srcfile2 \
{debug shlib libs=-ldl}] == -1 } {
return
}
# Now compile the inferior executable.
if {[build_executable "build executable" $testfile $srcfile] == -1} {
return
}
# Spawn GDB with LIBOBJ preloaded using LD_PRELOAD.
save_vars { env(LD_PRELOAD) env(ASAN_OPTIONS) } {
if { ![info exists env(LD_PRELOAD) ]
|| $env(LD_PRELOAD) == "" } {
set env(LD_PRELOAD) "$libobj"
} else {
append env(LD_PRELOAD) ":$libobj"
}
# Prevent address sanitizer error:
# ASan runtime does not come first in initial library list; you should
# either link runtime to your application or manually preload it with
# LD_PRELOAD.
append_environment_default ASAN_OPTIONS verify_asan_link_order 0
clean_restart $binfile
# Start GDB with the modified environment, this means that, when
# using remote targets, gdbserver will also use the preload
# library.
if {![runto_main]} {
return
}
}
gdb_breakpoint breakpt
gdb_continue_to_breakpoint "run to breakpt"
# Check the /proc/PID/smaps file itself. The call to 'cat' should
# inherit the preload library, so should see the modified file
# contents. Check that the shared memory mapping line has an id of
# zero. This confirms that the preload library is working. If the
# preload library breaks then we'll start seeing non-zero shared
# memory ids, which always worked, so we'd never know that this test
# is broken!
#
# This check ensures the test is working as expected.
set shmem_line_count 0
set fixup_line_count 0
set inf_pid [get_inferior_pid]
gdb_test_multiple "shell cat /proc/${inf_pid}/smaps" "check smaps" {
-re "^\\\[LD_PRELOAD\\\] updated a shared memory mapping\r\n" {
incr fixup_line_count
exp_continue
}
-re "^\[^\r\n\]+($decimal)\\s+/SYSV\[0-9\]{8}(?: \\(deleted\\))?\r\n" {
set id $expect_out(1,string)
if { $id == 0 } {
incr shmem_line_count
}
exp_continue
}
-re "^$gdb_prompt $" {
with_test_prefix $gdb_test_name {
gdb_assert { $shmem_line_count == 1 } \
"single shared memory mapping found"
gdb_assert { $fixup_line_count == 1 } \
"single fixup line found"
}
}
-re "^\[^\r\n\]+\r\n" {
exp_continue
}
}
# Now generate a core file. This will use the preload library to read
# the smaps file. The code below is copied from 'proc gdb_gcore_cmd',
# but we don't use that as we also look for a message that is printed
# by the LD_PRELOAD library. This is an extra level of check that the
# preload library is triggering when needed.
set corefile [standard_output_file ${testfile}.core]
set saw_ld_preload_msg false
set saw_saved_msg false
with_timeout_factor 3 {
gdb_test_multiple "gcore $corefile" "save core file" {
-re "^\\\[LD_PRELOAD\\\] updated a shared memory mapping\r\n" {
# GDB actually reads the smaps file multiple times when
# creating a core file, so we'll see multiple of these
# fixup lines.
set saw_ld_preload_msg true
exp_continue
}
-re "^Saved corefile \[^\r\n\]+\r\n" {
set saw_saved_msg true
exp_continue
}
-re "^$gdb_prompt $" {
with_test_prefix $gdb_test_name {
gdb_assert { $saw_saved_msg } \
"saw 'Saved corefile' message"
# If we're using a remote target then the message from
# the preload library will go to gdbservers stdout,
# not GDB's, so don't check for it.
if { [gdb_protocol_is_native] } {
gdb_assert { $saw_ld_preload_msg } \
"saw LD_PRELOAD message from library"
}
}
}
-re "^\[^\r\n\]*\r\n" {
exp_continue
}
}
}
# Restart GDB. This time we are _not_ using the preload library. We
# no longer need it as we are only analysing the core file now.
clean_restart $binfile
# Load the core file.
gdb_test "core-file $corefile" \
"Program terminated with signal SIGTRAP, Trace/breakpoint trap\\..*" \
"load core file"
# Look through the mappings. We _should_ see the shared memory
# mapping. We _should_not_ see any of the special '[blah]' style
# mappings, e.g. [vdso], [vstack], [vsyscalls], etc.
set saw_special_mapping false
set saw_shmem_mapping false
gdb_test_multiple "info proc mappings" "" {
-re "\r\nStart Addr\[^\r\n\]+File\\s*\r\n" {
exp_continue
}
-re "^$hex\\s+$hex\\s+$hex\\s+$hex\\s+\\\[\\S+\\\]\\s*\r\n" {
set saw_special_mapping true
exp_continue
}
-re "^$hex\\s+$hex\\s+$hex\\s+$hex\\s+/SYSV\[0-9\]+ \\(deleted\\)\\s*\r\n" {
set saw_shmem_mapping true
exp_continue
}
-re "^$hex\\s+$hex\\s+$hex\\s+$hex\[^\r\n\]*\r\n" {
exp_continue
}
-re "^$gdb_prompt $" {
with_test_prefix $gdb_test_name {
gdb_assert { $saw_shmem_mapping } \
"check shared memory mapping exists"
gdb_assert { !$saw_special_mapping } \
"check no special mappings added"
}
}
}