Repository: pwndbg/pwndbg
Pull request: Fix remote procinfo errors & remote xinfo
This provides a fix to #2615. The `procinfo` commands attempts to read SELinux attributes from a `/proc`, and reading that file ( `/proc/%PID/task/$TID/attr/current`) sometimes fails (you can reproduce locally with ` cat /proc/$$/attr/current`). There's some discussion of this behavior here - https://serverfault.com/questions/895946/cant-read-from-proc-self-attr-current-but-permissions-are-0666.

This adds a try-catch block around that code so that `procinfo` continues if the file cannot be read.


#### Fix to `xinfo`

This also reverts a previous change that prevents `xinfo` from producing full information when inspecting an address in a memory mapped file while debugging with qemu - https://github.com/pwndbg/pwndbg/commit/914110b8a3cfa7ebaa304c1288efe45151284a7c. After the commit linked here, the codebase has added changes so that we can get files from qemu processes, so we can now produce some elf and offset information about the address in question.

Before:
![image](https://github.com/user-attachments/assets/072f3818-216e-4928-af71-e845a1a61e1c)

After:

![image](https://github.com/user-attachments/assets/c49f5dfb-c62c-4b76-8066-7eb5122b1e46)


Problem statement:
procinfo could not download remote file
### Description

```
pwndbg> procinfo
exe        '/ctf/lake/order66/order66'
cmdline    /ctf/lake/order66/order66 ''
cwd        '/ctf/lake/order66'
pid        26861
tid        26861
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ ------------------------/pwndbg/pwndbg/aglib/file.py:79 in get_file                              │
│                                                                                                  │
│    76 │   │   │   │   return local_path                                                          │
│    77 │   │   │   local_path = tempfile.mktemp(dir=remote_files_dir())                           │
│    78 │   │   │   try:                                                                           │
│ ❱  79 │   │   │   │   pwndbg.dbg.selected_inferior().download_remote_file(path, local_path)      │
│    80 │   │   │   except pwndbg.dbg_mod.Error as e:                                              │
│    81 │   │   │   │   # This module originally raised this as an OSError.                        │
│    82 │   │   │   │   raise OSError(e)                                                           │
│                                                                                                  │
│ ------------------------/pwndbg/pwndbg/dbg/gdb.py:551 in download_remote_file                    │
│                                                                                                  │
│    548 │   │   │   │   │   real_error.append(line)                                               │
│    549 │   │   │   if len(real_error):                                                           │
│    550 │   │   │   │   error = "\n".join(real_error)                                             │
│ ❱  551 │   │   │   │   raise pwndbg.dbg_mod.Error(                                               │
│    552 │   │   │   │   │   "Could not download remote file %r:\nError: %s" % (remote_path, erro  │
│    553 │   │   │   │   )                                                                         │
│    554                                                                                           │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Error: Could not download remote file '/proc/26861/task/26861/attr/current':
Error: Remote I/O error: Invalid argument

During handling of the above exception, another exception occurred:

╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ ------------------------/pwndbg/pwndbg/commands/__init__.py:189 in __call__                      │
│                                                                                                  │
│   186 │                                                                                          │
│   187 │   def __call__(self, *args: Any, **kwargs: Any) -> str | None:                           │
│   188 │   │   try:                                                                               │
│ ❱ 189 │   │   │   return self.function(*args, **kwargs)                                          │
│   190 │   │   except TypeError:                                                                  │
│   191 │   │   │   print(f"{self.function.__name__.strip()!r}: {self.function.__doc__.strip()}"   │
│   192 │   │   │   pwndbg.exception.handle(self.function.__name__)                                │
│                                                                                                  │
│ ------------------------/pwndbg/pwndbg/commands/__init__.py:423 in _OnlyWhenRunning              │
│                                                                                                  │
│   420 │   def _OnlyWhenRunning(*a: P.args, **kw: P.kwargs) -> Optional[T]:                       │
│   421 │   │   # TODO: Properly support OnlyWhenRunning without `gdblib`.                         │
│   422 │   │   if pwndbg.aglib.proc.alive:                                                        │
│ ❱ 423 │   │   │   return function(*a, **kw)                                                      │
│   424 │   │   else:                                                                              │
│   425 │   │   │   log.error(f"{function.__name__}: The program is not being run.")               │
│   426 │   │   │   return None                                                                    │
│                                                                                                  │
│ ------------------------/pwndbg/pwndbg/commands/procinfo.py:265 in procinfo                      │
│                                                                                                  │
│   262 │   print("%-10s %s" % ("pid", proc.pid))                                                  │
│   263 │   print("%-10s %s" % ("tid", proc.tid))                                                  │
│   264 │                                                                                          │
│ ❱ 265 │   if proc.selinux != "unconfined":                                                       │
│   266 │   │   print("%-10s %s" % ("selinux", proc.selinux))                                      │
│   267 │                                                                                          │
│   268 │   print("%-10s %s" % ("ppid", proc.ppid))                                                │
│                                                                                                  │
│ ------------------------/pwndbg/pwndbg/lib/cache.py:162 in decorator                             │
│                                                                                                  │
│   159 │   │   │   │   if cached_value is not _NOT_FOUND_IN_CACHE:                                │
│   160 │   │   │   │   │   return cached_value                                                    │
│   161 │   │   │   │                                                                              │
│ ❱ 162 │   │   │   │   value = func(*a, **kw)                                                     │
│   163 │   │   │   │                                                                              │
│   164 │   │   │   │   # Sanity check: its not perfect and won't cover all cases like ([],)       │
│   165 │   │   │   │   # but it should be good enough                                             │
│                                                                                                  │
│ ------------------------/pwndbg/pwndbg/commands/procinfo.py:110 in selinux                       │
│                                                                                                  │
│   107 │   @pwndbg.lib.cache.cache_until("stop")                                                  │
│   108 │   def selinux(self):                                                                     │
│   109 │   │   path = "/proc/%i/task/%i/attr/current" % (self.pid, self.tid)                      │
│ ❱ 110 │   │   raw = pwndbg.aglib.file.get(path)                                                  │
│   111 │   │   return raw.decode().rstrip("\x00").strip()                                         │
│   112 │                                                                                          │
│   113 │   @property                                                                              │
│                                                                                                  │
│ ------------------------/pwndbg/pwndbg/aglib/file.py:101 in get                                  │
│                                                                                                  │
│    98 │   Returns:                                                                               │
│    99 │   │   A byte array, or None.                                                             │
│   100 │   """                                                                                    │
│ ❱ 101 │   local_path = get_file(path)                                                            │
│   102 │                                                                                          │
│   103 │   try:                                                                                   │
│   104 │   │   with open(local_path, "rb") as f:                                                  │
│                                                                                                  │
│ ------------------------/pwndbg/pwndbg/aglib/file.py:82 in get_file                              │
│                                                                                                  │
│    79 │   │   │   │   pwndbg.dbg.selected_inferior().download_remote_file(path, local_path)      │
│    80 │   │   │   except pwndbg.dbg_mod.Error as e:                                              │
│    81 │   │   │   │   # This module originally raised this as an OSError.                        │
│ ❱  82 │   │   │   │   raise OSError(e)                                                           │
│    83 │   │   else:                                                                              │
│    84 │   │   │   print(                                                                         │
│    85 │   │   │   │   message.warn(                                                              │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
OSError: Could not download remote file '/proc/26861/task/26861/attr/current':
Error: Remote I/O error: Invalid argument
```

### Steps to reproduce

Not sure. I was debugging a jailed docker container, but it seems to work now.

### My setup
Gdb:      15.2
Python:   3.12.7
Pwndbg:   2024.08.29 build: 1c3e9507
Capstone: 5.0.1280
Unicorn:  2.1.0

Hints from issue discussion:
We could catch the error and display `selinux: Not available due to error: {e}`. This approach could also be applied in other places where `get_file(/proc/)` is used.

Is this fixed already maybe? I think we had some changes to this... @patryk4815 
I think it is not fixed 
I don't know much about selinux but there's some funky stuff going on here.
The procinfo command has this:
```python
    if proc.selinux != "unconfined":
        print("%-10s %s" % ("selinux", proc.selinux))
```
which calls this:
```python
    @property
    @pwndbg.lib.cache.cache_until("stop")
    def selinux(self):
        path = "/proc/%i/task/%i/attr/current" % (self.pid, self.tid)
        raw = pwndbg.aglib.file.get(path)
        return raw.decode().rstrip("\x00").strip()
```
which calls the get (I added the print):
```python
def get(path: str) -> bytes:
    """
    Retrieves the contents of the specified file on the system
    where the current process is being debugged.

    Returns:
        A byte array, or None.
    """
    local_path = get_file(path)

    try:
        with open(local_path, "rb") as f:
            return f.read()
    except Exception as e:
        print("couldn't open file in get cuz: ", e) # added line for testing
        return b""
```
Heres what running the command looks like (debugging a local binary):
```
pwndbg> procinfo
exe        '/usr/bin/sh'
cmdline    /usr/bin/sh ''
cwd        '/home/user/opt/pwndbg'
pid        2828
tid        2828
couldn't open file in get cuz:  [Errno 22] Invalid argument
selinux    
ppid       2806
uid        [1000, 1000, 1000, 1000]
gid        [1000, 1000, 1000, 1000]
groups     [19, 90, 96, 98, 972, 984, 985, 987, 990, 991, 996, 998, 1000]
fd[0]      /dev/pts/3
fd[1]      /dev/pts/3
fd[2]      /dev/pts/3
fd[255]    /dev/pts/3
```
If I try to cat the proc file:
```
~> cat /proc/2828/task/2828/attr/current
cat: /proc/2828/task/2828/attr/current: Invalid argument
~> cat /proc/2828/attr/current
cat: /proc/2828/attr/current: Invalid argument
~> 
```
The intent is probably that `selinux` shouldn't be printed if selinux is unused, rather than an empty string being printed (like what is happening here). Is there a "valid" way to open the /current proc-file?

The exception occured for me because `get_file` goes into a branch for remote debugging which raises an error when it can't open a file:
```python
            try:
                pwndbg.dbg.selected_inferior().download_remote_file(path, local_path)
            except pwndbg.dbg_mod.Error as e:
                # This module originally raised this as an OSError.
                raise OSError(e)
```
while `get()` which just returns `b""`.
Is it fixed?
Dont think so, I encountered this issue a few days ago

Commit message: Fix remote procinfo error due to error in opening SELinux attribute file. Bring full functionality to xinfo on remote hosts

Patch to review:
diff --git a/pwndbg/commands/procinfo.py b/pwndbg/commands/procinfo.py
index 4e2d287c9a6..5581e93b517 100644
--- a/pwndbg/commands/procinfo.py
+++ b/pwndbg/commands/procinfo.py
@@ -105,10 +105,14 @@ def __init__(self, pid=None, tid=None) -> None:
 
     @property
     @pwndbg.lib.cache.cache_until("stop")
-    def selinux(self):
+    def selinux(self) -> str:
         path = "/proc/%i/task/%i/attr/current" % (self.pid, self.tid)
-        raw = pwndbg.aglib.file.get(path)
-        return raw.decode().rstrip("\x00").strip()
+        try:
+            raw = pwndbg.aglib.file.get(path)
+            return raw.decode().rstrip("\x00").strip()
+        except OSError:
+            # This file sometimes cannot be read. This indicates SELinux is not enabled
+            return ""
 
     @property
     @pwndbg.lib.cache.cache_until("stop")
@@ -262,7 +266,8 @@ def procinfo() -> None:
     print("%-10s %s" % ("pid", proc.pid))
     print("%-10s %s" % ("tid", proc.tid))
 
-    if proc.selinux != "unconfined":
+    # Indicate if the process is restricted by SELinux policies
+    if proc.selinux != "" and proc.selinux != "unconfined":
         print("%-10s %s" % ("selinux", proc.selinux))
 
     print("%-10s %s" % ("ppid", proc.ppid))
diff --git a/pwndbg/commands/xinfo.py b/pwndbg/commands/xinfo.py
index 863ed7d0402..6da510ffbb7 100644
--- a/pwndbg/commands/xinfo.py
+++ b/pwndbg/commands/xinfo.py
@@ -1,7 +1,6 @@
 from __future__ import annotations
 
 import argparse
-import os
 
 import pwndbg
 import pwndbg.aglib.arch
@@ -13,6 +12,7 @@
 import pwndbg.commands
 import pwndbg.wrappers
 from pwndbg.commands import CommandCategory
+from pwndbg.lib.memory import Page
 
 parser = argparse.ArgumentParser(
     description="Shows offsets of the specified address from various useful locations."
@@ -26,7 +26,7 @@ def print_line(name, addr, first, second, op, width=20) -> None:
     )
 
 
-def xinfo_stack(page, addr) -> None:
+def xinfo_stack(page: Page, addr: int) -> None:
     # If it's a stack address, print offsets to top and bottom of stack, as
     # well as offsets to current stack and base pointer (if used by debuggee)
 
@@ -55,15 +55,11 @@ def xinfo_stack(page, addr) -> None:
             print_line("Next Stack Canary", addr, nxt, nxt - addr, "-")
 
 
-def xinfo_mmap_file(page, addr) -> None:
+def xinfo_mmap_file(page: Page, addr: int) -> None:
     # If it's an address pointing into a memory mapped file, print offsets
     # to beginning of file in memory and on disk
 
     file_name = page.objfile
-    # Check if the file exists on the local system, as we may be attached to a gdb-server or qemu.
-    # Even so, the file might exist locally, if we are doing `target remote localhost:1234`
-    if not os.path.exists(file_name):
-        return None
 
     objpages = filter(lambda p: p.objfile == file_name, pwndbg.aglib.vmmap.get())
     first = sorted(objpages, key=lambda p: p.vaddr)[0]
@@ -100,7 +96,7 @@ def xinfo_mmap_file(page, addr) -> None:
             print_line(sec["x_name"], addr, sec["sh_addr"], addr - sec["sh_addr"], "+")
 
 
-def xinfo_default(page, addr) -> None:
+def xinfo_default(page: Page, addr: int) -> None:
     # Just print the distance to the beginning of the mapping
     print_line("Mapped Area", addr, page.vaddr, addr - page.vaddr, "+")
 
diff --git a/pwndbg/dbg/gdb/__init__.py b/pwndbg/dbg/gdb/__init__.py
index e97ad5d82eb..ea2a7125ebd 100644
--- a/pwndbg/dbg/gdb/__init__.py
+++ b/pwndbg/dbg/gdb/__init__.py
@@ -596,10 +596,15 @@ def download_remote_file(self, remote_path: str, local_path: str) -> None:
         import pwndbg.aglib.file
 
         if pwndbg.aglib.file.is_vfile_qemu_user_bug():
-            with open(local_path, "wb") as fp:
-                for data in pwndbg.aglib.file.vfile_readfile(remote_path):
-                    fp.write(data)
-            return
+            try:
+                with open(local_path, "wb") as fp:
+                    for data in pwndbg.aglib.file.vfile_readfile(remote_path):
+                        fp.write(data)
+                return
+            except OSError as e:
+                raise pwndbg.dbg_mod.Error(
+                    "Could not download remote file %r:\nError: %s" % (remote_path, str(e))
+                )
 
         try:
             error = gdb.execute(f'remote get "{remote_path}" "{local_path}"', to_string=True)
diff --git a/pwndbg/lib/memory.py b/pwndbg/lib/memory.py
index ec7871a2d01..25d67a01ab7 100644
--- a/pwndbg/lib/memory.py
+++ b/pwndbg/lib/memory.py
@@ -59,6 +59,12 @@ class Page:
     flags = 0  #: Flags set by the ELF file, see PF_X, PF_R, PF_W
     offset = 0  #: Offset into the original ELF file that the data is loaded from
     objfile = ""  #: Path to the ELF on disk
+    """
+    Possible non-empty values of `objfile`:
+    - Contains square brackets "[]" if it's not a memory mapped file.
+        Examples: [stack], [vsyscall], [heap], [vdso]
+    - A path to a file, such as `/usr/lib/libc.so.6`
+    """
 
     def __init__(self, start: int, size: int, flags: int, offset: int, objfile: str = "") -> None:
         self.vaddr = start


Code review comments:
diff_hunk: @@ -596,10 +596,15 @@ def download_remote_file(self, remote_path: str, local_path: str) -> None:
         import pwndbg.aglib.file
 
         if pwndbg.aglib.file.is_vfile_qemu_user_bug():
-            with open(local_path, "wb") as fp:
-                for data in pwndbg.aglib.file.vfile_readfile(remote_path):
-                    fp.write(data)
-            return
+            try:
+                with open(local_path, "wb") as fp:
+                    for data in pwndbg.aglib.file.vfile_readfile(remote_path):
+                        fp.write(data)
+                return
+            except OSError as e:
+                raise pwndbg.dbg_mod.Error(
+                    "Could not download remote file %r:\nError: %s" % (remote_path, str(e))
+                )
path: pwndbg/dbg/gdb/__init__.py
text: @author:
The other portions of this function have try-catch blocks on other methods of getting files, so this is added here to make it clear this can produce an error as well.

@user1:
🤔 this shouldn't produce error?, `local_path` should be clean file in /tmp right?
or you mean `vfile_readfile` can trigger error?


@author:
Yes - vfile_readfile can raise an error because it calls vfile_pread which will raise an OSError on file read error. This catch blocks converts that OSError into a pwndbg.dbg_mod.Error like the other try/catch blocks in this function.

@author:
I see - I can move the try-catch block inside the `with open()` statement. I'll make that change now.