]> git.ipfire.org Git - thirdparty/binutils-gdb.git/commitdiff
gdbserver: add vFile:lstat packet support
authorAndrew Burgess <aburgess@redhat.com>
Wed, 11 Jun 2025 19:01:56 +0000 (20:01 +0100)
committerAndrew Burgess <aburgess@redhat.com>
Tue, 17 Jun 2025 20:21:32 +0000 (21:21 +0100)
In the following commits I added the target_fileio_stat function, and
the target_ops::fileio_stat member function:

  * 08a115cc1c4 gdb: add target_fileio_stat, but no implementations yet
  * 3055e3d2f13 gdb: add GDB side target_ops::fileio_stat implementation
  * 6d45af96ea5 gdbserver: add gdbserver support for vFile::stat packet
  * 22836ca8859 gdb: check for multiple matching build-id files

Unfortunately I messed up, despite being called 'stat' these function
actually performed an 'lstat'.  The 'lstat' is the correct (required)
implementation, it's the naming that is wrong.

In the previous commit I fixed the naming within GDB, renaming 'stat'
to 'lstat' throughout.

However, in order to support target_fileio_stat (as was) on remote
targets, the above patches added the vFile:stat packet, which actually
performed an 'lstat' call.  This is really quite unfortunate, and I'd
like to do as much as I can to try and clean up this mess.  But I'm
mindful that changing packets is not really the done thing.

So, this commit doesn't change anything.

Instead, this commit adds vFile:lstat as a new packet.

Currently, this packet is handled identically as vFile:stat, the
packet performs an 'lstat' call.

I then update GDB to send the new vFile:lstat instead of vFile:stat
for the remote_target::fileio_lstat implementation.

After this commit GDB will never send the vFile:stat packet.

However, I have retained the 'set remote hostio-stat-packet' control
flag, just in case someone was trying to set this somewhere.

Then there's one test in the testsuite which used to disable the
vFile:stat packet, that test is updated to now disable vFile:lstat.

There's a new test that does a more direct test of vFile:lstat.  This
new test can be extended to also test vFile:stat, but that is left for
the next commit.

And so, after this commit, GDB sends the new vFile:lstat packet in
order to implement target_ops::fileio_lstat.  The new packet is more
clearly documented than vFile:stat is.  But critically, this change
doesn't risk breaking any other clients or servers that implement
GDB's remote protocol.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
Approved-By: Tom Tromey <tom@tromey.com>
gdb/NEWS
gdb/doc/gdb.texinfo
gdb/remote.c
gdb/testsuite/gdb.server/build-id-seqno.exp
gdb/testsuite/gdb.server/fileio-packets.exp [new file with mode: 0644]
gdb/testsuite/gdb.server/fileio-packets.py [new file with mode: 0644]
gdbserver/hostio.cc

index 4dcc344b07278654b1d54ca71db6749d62890d01..1b5e1e89534d62bbd59b494243addcf29c70e96a 100644 (file)
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -182,6 +182,11 @@ binary-upload in qSupported reply
   stub doesn't report this feature supported, then GDB will not use
   the 'x' packet.
 
+vFile:lstat
+  Return information about files on the remote system.  Like
+  vFile:stat but if the filename is a symbolic link, return
+  information about the link itself, the file the link refers to.
+
 * Changed remote packets
 
 qXfer:threads:read
index c41aa89a3c4821b6296c338a03d6eef699903057..a564307053eccb83259c1d1d1e8cc7c4b5d69c60 100644 (file)
@@ -24782,6 +24782,10 @@ future connections is shown.  The available settings are:
 @tab @code{vFile:stat}
 @tab Host I/O
 
+@item @code{hostio-lstat-packet}
+@tab @code{vFile:lstat}
+@tab Host I/O
+
 @item @code{hostio-setfs-packet}
 @tab @code{vFile:setfs}
 @tab Host I/O
@@ -46732,6 +46736,17 @@ and the return value is the size of this attachment in bytes.
 If an error occurs the return value is -1.  The format of the
 returned binary attachment is as described in @ref{struct stat}.
 
+@item vFile:lstat: @var{filename}
+Get information about the file @var{filename} on the target as if from
+an @samp{lstat} call.  On success the information is returned as a
+binary attachment and the return value is the size of this attachment
+in bytes.  If an error occurs the return value is -1.  The format of
+the returned binary attachment is as described in @ref{struct stat}.
+
+If @var{filename} is a symbolic link, then this packet returns
+information about the link itself, not the file that the link refers
+to, this is inline with the @samp{lstat} library call.
+
 @item vFile:unlink: @var{filename}
 Delete the file at @var{filename} on the target.  Return 0,
 or -1 if an error occurs.  The @var{filename} is a string.
index 92d428c63d0778e28066b4730bf2dad3234b6c8d..8c3040f57ab0d20f42909e9eb550eb517e646f73 100644 (file)
@@ -251,6 +251,7 @@ enum {
   PACKET_vFile_readlink,
   PACKET_vFile_fstat,
   PACKET_vFile_stat,
+  PACKET_vFile_lstat,
   PACKET_qXfer_auxv,
   PACKET_qXfer_features,
   PACKET_qXfer_exec_file,
@@ -13242,14 +13243,14 @@ remote_target::fileio_lstat (struct inferior *inf, const char *filename,
   if (remote_hostio_set_filesystem (inf, remote_errno) != 0)
     return {};
 
-  remote_buffer_add_string (&p, &left, "vFile:stat:");
+  remote_buffer_add_string (&p, &left, "vFile:lstat:");
 
   remote_buffer_add_bytes (&p, &left, (const gdb_byte *) filename,
                           strlen (filename));
 
   int attachment_len;
   const char *attachment;
-  int ret = remote_hostio_send_command (p - rs->buf.data (), PACKET_vFile_stat,
+  int ret = remote_hostio_send_command (p - rs->buf.data (), PACKET_vFile_lstat,
                                        remote_errno, &attachment,
                                        &attachment_len);
 
@@ -16422,6 +16423,8 @@ Show the maximum size of the address (in bits) in a memory packet."), NULL,
 
   add_packet_config_cmd (PACKET_vFile_stat, "vFile:stat", "hostio-stat", 0);
 
+  add_packet_config_cmd (PACKET_vFile_lstat, "vFile:lstat", "hostio-lstat", 0);
+
   add_packet_config_cmd (PACKET_vAttach, "vAttach", "attach", 0);
 
   add_packet_config_cmd (PACKET_vRun, "vRun", "run", 0);
index a508a4454b7cd2c0937d3b4294e190fb376059df..8475cccfcc3b0c3f1d5f9b59ed3abf0bd9ed3315 100644 (file)
@@ -90,13 +90,13 @@ proc load_binfile_check_debug_is_found { debuginfo_file testname } {
     with_test_prefix "$testname" {
        with_timeout_factor 5 {
            # Probing for .build-id based debug files on remote
-           # targets uses the vFile:stat packet by default, though
+           # targets uses the vFile:lstat packet by default, though
            # there is a work around that avoids this which can be
            # used if GDB is connected to an older gdbserver without
            # 'stat' support.
            #
            # Check the work around works by disabling use of the
-           # vFile:stat packet.
+           # vFile:lstat packet.
            foreach_with_prefix stat_pkt {auto off} {
                clean_restart
 
@@ -105,7 +105,7 @@ proc load_binfile_check_debug_is_found { debuginfo_file testname } {
 
                gdb_test_no_output "set sysroot target:"
 
-               gdb_test "set remote hostio-stat-packet $stat_pkt"
+               gdb_test "set remote hostio-lstat-packet $stat_pkt"
 
                # Make sure we're disconnected, in case we're testing with an
                # extended-remote board, therefore already connected.
diff --git a/gdb/testsuite/gdb.server/fileio-packets.exp b/gdb/testsuite/gdb.server/fileio-packets.exp
new file mode 100644 (file)
index 0000000..d243e93
--- /dev/null
@@ -0,0 +1,60 @@
+# 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/>.
+
+# Test some remote file I/O.  The associated Python script uses the
+# Python API to create and send vFile:* packets to gdbserver to
+# perform actions like 'stat'.  The same action is then performed
+# directly from Python (e.g. a 'stat' is performed), and the results,
+# from gdbserver, and from the local syscall, are compared.
+
+load_lib gdb-python.exp
+load_lib gdbserver-support.exp
+
+require allow_python_tests
+require allow_gdbserver_tests
+require {!is_remote host}
+require {!is_remote target}
+
+standard_testfile
+
+clean_restart
+
+# Make sure we're disconnected, in case we're testing with an
+# extended-remote board, therefore already connected.
+gdb_test "disconnect" ".*"
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+gdb_test_no_output "source $pyfile" "source the script"
+
+# Start gdbserver, but always in extended-remote mode, and then
+# connect to it from GDB.
+set res [gdbserver_start "--multi --once" ""]
+set gdbserver_protocol "extended-remote"
+set gdbserver_gdbport [lindex $res 1]
+gdb_target_cmd $gdbserver_protocol $gdbserver_gdbport
+
+gdb_test_no_output "set python print-stack full"
+
+set test_file_1 [standard_output_file "test_file_1"]
+remote_exec host "touch $test_file_1"
+
+set test_file_2 [standard_output_file "test_file_2"]
+remote_exec host "ln -s $test_file_1 $test_file_2"
+
+gdb_test "python check_lstat(\"$test_file_1\")" "PASS" \
+    "check remote lstat works on a normal file"
+
+gdb_test "python check_lstat(\"$test_file_2\")" "PASS" \
+    "check remote lstat works on a symbolic link"
diff --git a/gdb/testsuite/gdb.server/fileio-packets.py b/gdb/testsuite/gdb.server/fileio-packets.py
new file mode 100644 (file)
index 0000000..53fb49e
--- /dev/null
@@ -0,0 +1,170 @@
+# Copyright (C) 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/>.
+
+import os, stat
+
+
+# Hex encode INPUT_STRING in the same way that GDB does.  Each
+# character in INPUT_STRING is expanded to its two digit hex
+# representation in the returned string.
+#
+# Only ASCII characters may appear in INPUT_STRING, this is more
+# restrictive than GDB, but is good enough for testing.
+def hex_encode(input_string):
+    byte_string = input_string.encode("ascii")
+    hex_string = byte_string.hex()
+    return hex_string
+
+
+# Binary remote data packets can contain some escaped bytes.  Decode
+# the packet now.
+def unescape_remote_data(buf):
+    escaped = False
+    res = bytearray()
+    for b in buf:
+        if escaped:
+            res.append(b ^ 0x20)
+            escaped = False
+        elif b == ord("}"):
+            escaped = True
+        else:
+            res.append(b)
+    res = bytes(res)
+    return res
+
+
+# Decode the results of a remote stat like command from BUF.  Returns
+# None if BUF is not a valid stat result (e.g. if it indicates an
+# error, or the buffer is too short).  If BUF is valid then the fields
+# are decoded according to the GDB remote protocol and placed into a
+# dictionary, this dictionary is then returned.
+def decode_stat_reply(buf, byteorder="big"):
+
+    buf = unescape_remote_data(buf)
+
+    if (
+        buf[0] != ord("F")
+        or buf[1] != ord("4")
+        or buf[2] != ord("0")
+        or buf[3] != ord(";")
+        or len(buf) != 68
+    ):
+        l = len(buf)
+        print(f"decode_stat_reply failed: {buf}\t(length = {l})")
+        return None
+
+    # Discard the 'F40;' prefix.  The rest is the 64 bytes of data to
+    # be decoded.
+    buf = buf[4:]
+
+    st_dev = int.from_bytes(buf[0:4], byteorder=byteorder)
+    st_ino = int.from_bytes(buf[4:8], byteorder=byteorder)
+    st_mode = int.from_bytes(buf[8:12], byteorder=byteorder)
+    st_nlink = int.from_bytes(buf[12:16], byteorder=byteorder)
+    st_uid = int.from_bytes(buf[16:20], byteorder=byteorder)
+    st_gid = int.from_bytes(buf[20:24], byteorder=byteorder)
+    st_rdev = int.from_bytes(buf[24:28], byteorder=byteorder)
+    st_size = int.from_bytes(buf[28:36], byteorder=byteorder)
+    st_blksize = int.from_bytes(buf[36:44], byteorder=byteorder)
+    st_blocks = int.from_bytes(buf[44:52], byteorder=byteorder)
+    st_atime = int.from_bytes(buf[52:56], byteorder=byteorder)
+    st_mtime = int.from_bytes(buf[56:60], byteorder=byteorder)
+    st_ctime = int.from_bytes(buf[60:64], byteorder=byteorder)
+
+    return {
+        "st_dev": st_dev,
+        "st_ino": st_ino,
+        "st_mode": st_mode,
+        "st_nlink": st_nlink,
+        "st_uid": st_uid,
+        "st_gid": st_gid,
+        "st_rdev": st_rdev,
+        "st_size": st_size,
+        "st_blksize": st_blksize,
+        "st_blocks": st_blocks,
+        "st_atime": st_atime,
+        "st_mtime": st_mtime,
+        "st_ctime": st_ctime,
+    }
+
+
+# Perform an lstat of remote file FILENAME, and create a dictionary of
+# the results, the keys are the fields of the stat structure.
+def remote_lstat(filename):
+    conn = gdb.selected_inferior().connection
+    if not isinstance(conn, gdb.RemoteTargetConnection):
+        raise gdb.GdbError("connection is the wrong type")
+
+    filename_hex = hex_encode(filename)
+    reply = conn.send_packet("vFile:lstat:%s" % filename_hex)
+
+    stat = decode_stat_reply(reply)
+    return stat
+
+
+# Convert a stat_result object to a dictionary that should match the
+# dictionary built from the remote protocol reply.
+def stat_result_to_dict(res):
+    # GDB doesn't support the S_IFLNK flag for the remote protocol, so
+    # clear that flag in the local results.
+    if stat.S_ISLNK(res.st_mode):
+        st_mode = stat.S_IMODE(res.st_mode)
+    else:
+        st_mode = res.st_mode
+
+    # GDB returns an integer for these fields, while Python returns a
+    # floating point value.  Convert back to an integer to match GDB.
+    st_atime = int(res.st_atime)
+    st_mtime = int(res.st_mtime)
+    st_ctime = int(res.st_ctime)
+
+    return {
+        "st_dev": res.st_dev,
+        "st_ino": res.st_ino,
+        "st_mode": st_mode,
+        "st_nlink": res.st_nlink,
+        "st_uid": res.st_uid,
+        "st_gid": res.st_gid,
+        "st_rdev": res.st_rdev,
+        "st_size": res.st_size,
+        "st_blksize": res.st_blksize,
+        "st_blocks": res.st_blocks,
+        "st_atime": st_atime,
+        "st_mtime": st_mtime,
+        "st_ctime": st_ctime,
+    }
+
+
+# Perform an lstat of local file FILENAME, and create a dictionary of
+# the results, the keys are the fields of the stat structure.
+def local_lstat(filename):
+    res = os.lstat(filename)
+    return stat_result_to_dict(res)
+
+
+# Perform a remote lstat using GDB, and a local lstat using os.lstat.
+# Compare the results to check they are the same.
+#
+# For this test to work correctly, gdbserver, and GDB (where this
+# Python script is running), must see the same filesystem.
+def check_lstat(filename):
+    s1 = remote_lstat(filename)
+    s2 = local_lstat(filename)
+
+    print(f"s1 = {s1}")
+    print(f"s2 = {s2}")
+
+    assert s1 == s2
+    print("PASS")
index 17b6179d8ca65055209cbaed22c159c7576f22df..05f2c4e0b3387668a2d5b0c5024946abc7f1b9ea 100644 (file)
@@ -522,6 +522,42 @@ handle_stat (char *own_buf, int *new_packet_len)
     write_enn (own_buf);
 }
 
+static void
+handle_lstat (char *own_buf, int *new_packet_len)
+{
+  int bytes_sent;
+  char *p;
+  struct stat st;
+  struct fio_stat fst;
+  char filename[HOSTIO_PATH_MAX];
+
+  p = own_buf + strlen ("vFile:lstat:");
+
+  if (require_filename (&p, filename)
+      || require_end (p))
+    {
+      hostio_packet_error (own_buf);
+      return;
+    }
+
+  if (lstat (filename, &st) == -1)
+    {
+      hostio_error (own_buf);
+      return;
+    }
+
+  host_to_fileio_stat (&st, &fst);
+
+  bytes_sent = hostio_reply_with_data (own_buf,
+                                      (char *) &fst, sizeof (fst),
+                                      new_packet_len);
+
+  /* If the response does not fit into a single packet, do not attempt
+     to return a partial response, but simply fail.  */
+  if (bytes_sent < sizeof (fst))
+    write_enn (own_buf);
+}
+
 static void
 handle_close (char *own_buf)
 {
@@ -641,6 +677,8 @@ handle_vFile (char *own_buf, int packet_len, int *new_packet_len)
     handle_fstat (own_buf, new_packet_len);
   else if (startswith (own_buf, "vFile:stat:"))
     handle_stat (own_buf, new_packet_len);
+  else if (startswith (own_buf, "vFile:lstat:"))
+    handle_lstat (own_buf, new_packet_len);
   else if (startswith (own_buf, "vFile:close:"))
     handle_close (own_buf);
   else if (startswith (own_buf, "vFile:unlink:"))