Repository: Gallopsled/pwntools

Issue:
gdb.debug not working with fish shell
```python
from pwn import *

p = gdb.debug(['./start'])

p.interactive()
p.close()
```
Hangs with:
```
[+] Starting local process '/usr/bin/gdbserver': pid 5956
```
Regardless of if tmux is active. Tried on both kitty terminal and gnome terminal. Ctrl+C gives:
```
Traceback (most recent call last):
  File "/home/maker/ctf/pwnabletw/start/mvp.py", line 3, in <module>
    p = gdb.debug(['./start'])
        ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/maker/.local/lib/python3.11/site-packages/pwnlib/context/__init__.py", line 1581, in setter
    return function(*a, **kw)
           ^^^^^^^^^^^^^^^^^^
  File "/home/maker/.local/lib/python3.11/site-packages/pwnlib/gdb.py", line 574, in debug
    port = _gdbserver_port(gdbserver, ssh)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/maker/.local/lib/python3.11/site-packages/pwnlib/gdb.py", line 318, in _gdbserver_port
    process_created = gdbserver.recvline()
                      ^^^^^^^^^^^^^^^^^^^^
  File "/home/maker/.local/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 498, in recvline
    return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/maker/.local/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 341, in recvuntil
    res = self.recv(timeout=self.timeout)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/maker/.local/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 106, in recv
    return self._recv(numb, timeout) or b''
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/maker/.local/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 176, in _recv
    if not self.buffer and not self._fillbuffer(timeout):
                               ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/maker/.local/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 155, in _fillbuffer
    data = self.recv_raw(self.buffer.get_fill_size())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/maker/.local/lib/python3.11/site-packages/pwnlib/tubes/process.py", line 673, in recv_raw
    if not self.can_recv_raw(self.timeout):
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/maker/.local/lib/python3.11/site-packages/pwnlib/tubes/process.py", line 717, in can_recv_raw
    return select.select([self.proc.stdout], [], [], timeout) == ([self.proc.stdout], [], [])
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
KeyboardInterrupt
[*] Stopped process './start' (pid 5956)
```


Discussion:
This is due to fish forking multiple times when running `fish -c somescript` and we're returning the pid of the original fish process instead of the forked one which actually runs the script.

I don't use fish, so you're welcome to propose a pull request for the `misc.run_in_new_terminal` function to return the correct pid around:
https://github.com/Gallopsled/pwntools/blob/75cc3c3d5cd1e4d03dee1cf31ba67dd8139b941c/pwnlib/util/misc.py#L305
> This is due to fish forking multiple times when running `fish -c somescript` and we're returning the pid of the original fish process instead of the forked one which actually runs the script.

I only see one process here.

![image](https://github.com/Gallopsled/pwntools/assets/74560659/0a1c46ae-c5c4-4d6e-8147-ee2909604fce)

But your screenshot shows 3 processes?

When I run `fish -c "sleep 999"` I see the fish process staying alive instead of being replaced by the sleep binary

```
# fish -c "sleep 999"
# other terminal:
pstree -p `whoami` | grep sleep
zsh(367594)---fish(368747)---sleep(368753)
```

compared to bash or zsh:
```
# bash -c "sleep 999"
# other terminal:
pstree -p `whoami` | grep sleep
zsh(367594)---sleep(368783)
```

I've asked about this in fish's matrix channel and it's expected behavior - BUT I've confused myself with terminal emulators and shells. This issue isn't in pwntools since we don't run `shell -c command` at all but an instance of #2321 which boils down to a [bug in gdbserver](https://sourceware.org/bugzilla/show_bug.cgi?id=26116). pwntools 4.13.0 would print a more helpful error message suggesting you to set the SHELL environment variable to some other shell than fish like bash.

So try doing `p = gdb.debug(['./start'], env={"SHELL": "/bin/bash"})` instead.

Looking a bit more: gdbserver supports a `--no-startup-with-shell` argument which might help too.

```
  --startup-with-shell
                        Start PROG using a shell.  I.e., execs a shell that
                        then execs PROG.  (default)
  --no-startup-with-shell
                        Exec PROG directly instead of using a shell.
                        Disables argument globbing and variable substitution
                        on UNIX-like systems.
```

@Arusekk can you think of why we don't start the program directly but have it go through a shell in gdb.debug? We don't do that when starting directly using `process()` and I don't think we support the `shell=True` argument when debugging?

~@iacore~ @k4lizen  can you test if adding that flag fixes your issue please? in gdb.py in [_gdbserver_args()](https://github.com/Gallopsled/pwntools/blob/75cc3c3d5cd1e4d03dee1cf31ba67dd8139b941c/pwnlib/gdb.py#L326), can you add the argument?
Change that line to
```python
gdbserver_args = [gdbserver, '--multi', '--no-startup-with-shell']
```
in your `/home/maker/.local/lib/python3.11/site-packages/pwnlib/gdb.py`
> So try doing p = gdb.debug(['./start'], env={"SHELL": "/bin/bash"}) instead.

This does seem to work. 
> gdbserver_args = [gdbserver, '--multi', '--no-startup-with-shell']

This also seems to work.

A bit off topic but im seeing some weird behaviour with the `SHELL` env variable, like doing `fish; bash; fish;` points to bash sometimes. Or setting the variable to fish and going into zsh still has it pointing to fish. 

But anyways yeah, the proposed fixes seem good (and works with my template script with tmux settings etc).


> But your screenshot shows 3 processes?

I run `fish -c xxx` in fish shell (I use fish already).


> When I run `fish -c "sleep 999"` I see the fish process staying alive instead of being replaced by the sleep binary

This is expected. -c runs the script in script mode, not execv. You are likely looking for `fish -c 'exec sleep 999'`, which calls execv.

> compared to bash or zsh:
> ...

It's not mentioned in [sh](https://pubs.opengroup.org/onlinepubs/009695399/utilities/sh.html), so I guess it's not even part of POSIX. fish is also not a POSIX-compliant shell, so..
Yes, [faho](https://github.com/faho) mentioned this too and it seems the other shells are [doing it wrong](https://registerspill.thorstenball.com/p/how-to-lose-control-of-your-shell), but as I said, this is out of our control and done inside gdbserver.
No idea either, I guess nobody thought about it when it worked correctly (before fish even existed), and this is most likely GDB not supporting fish, not us not thinking :wink:

Resolving patch:
diff --git a/pwnlib/gdb.py b/pwnlib/gdb.py
--- a/pwnlib/gdb.py
+++ b/pwnlib/gdb.py
@@ -287,7 +287,9 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None):
 
     orig_args = args
 
-    gdbserver_args = [gdbserver, '--multi']
+    # --no-startup-with-shell is required for forking shells like SHELL=/bin/fish
+    # https://github.com/Gallopsled/pwntools/issues/2377
+    gdbserver_args = [gdbserver, '--multi', '--no-startup-with-shell']
     if context.aslr:
         gdbserver_args += ['--no-disable-randomization']
     else:


Test patch: