/* This testcase is part of GDB, the GNU debugger.

   Copyright 2025-2026 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');
}

/* Return true if PATHNAME has for form "/proc/PID/smaps" (without the
   quotes).  Otherwise, return false.  */

static bool
is_smaps_file (const char *pathname)
{
  if (strncmp (pathname, "/proc/", 6) == 0)
    {
      int idx = 6;
      while (isdigit (pathname[idx]))
	idx++;
      if (idx > 6 && strcmp (&pathname[idx], "/smaps") == 0)
	return true;
    }

  return false;
}

/* Return true if PATHNAME should be considered interesting.  PATHNAME is
   interesting if it has the form /proc/PID/smaps, and there is no
   interesting file already opened.  */

static bool
is_interesting_pathname (const char *pathname)
{
  return the_file.fd == -1 && is_smaps_file (pathname);
}

/* Read the contents of an interesting file from FD (and open file
   descriptor) into the global THE_FILE variable, making the file FD the
   current interesting file.  There should be no already open interesting
   file when this function is called.

   The contents of the file FD are read into a memory buffer and updated so
   that any shared memory mappings listed within FD (which will be an smaps
   file) will have the id zero.  */

static void
read_interesting_file_contents (int fd)
{
#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 (fd, 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 (fd, 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 = fd;

  update_file_content_buffer ();
#undef BLOCK_SIZE
}

/* 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 = is_interesting_pathname (pathname);

  /* 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);
    }

  /* 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 (res != -1 && is_interesting)
    read_interesting_file_contents (res);

  return res;
}

/* Like above, but for open64.  */

int
open64 (const char *pathname, int flags, ...)
{
  /* Pointer to the real open64 function.  */
  static open_func_type real_open64 = 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 = is_interesting_pathname (pathname);

  /* 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);
    }

  /* Debug.  */
  if (is_interesting)
    log_msg ("[LD_PRELOAD] Opening file: %s\n", pathname);

  /* Make sure we have a pointer to the real open64() function.  */
  if (real_open64 == NULL)
    {
      /* Get the address of the real open64() function.  */
      real_open64 = (open_func_type) dlsym (RTLD_NEXT, "open64");
      if (real_open64 == NULL)
	error ("[LD_PRELOAD] dlsym() error for 'open64': %s\n", dlerror ());
    }

  /* Call the original open64() function with the provided arguments.  */
  int res = -1;
  if (flags & O_CREAT)
    res = real_open64 (pathname, flags, mode);
  else
    res = real_open64 (pathname, flags);

  if (res != -1 && is_interesting)
    read_interesting_file_contents (res);

  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);
}
