Skip to main content

hermit/
gdbserver.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! The TCP port a gdbserver serves its one gdb client on, bound in the host's
10//! network namespace.
11//!
12//! The gdbserver runs in the tracer, inside the container. A port the tracer
13//! bound itself would be in the guest's network namespace, which a gdb client
14//! on the host cannot reach while the guest has a private network
15//! (`--network=local`). So the CLI binds the port before it makes the
16//! container (`GdbPort::bind`): a socket stays in the network namespace it
17//! was made in, and the tracer inherits it at the clone. Just before the
18//! tracer spawns the guest it listens on that socket (`connection`), and a
19//! relay accepts the one client and passes its bytes to the gdbserver, which
20//! listens on an abstract Unix socket in the tracer's own namespace. The guest
21//! keeps the network it was given, and a host gdb client reaches the port
22//! either way.
23//!
24//! The relay is a task on the tracer's Tokio runtime, not a thread: a thread
25//! in the tracer would take a PID in the container's PID namespace and shift
26//! every guest PID after it.
27
28use std::ffi::OsString;
29use std::io;
30use std::os::fd::AsRawFd;
31use std::os::fd::OwnedFd;
32use std::os::unix::ffi::OsStringExt;
33use std::path::Path;
34use std::path::PathBuf;
35use std::sync::Mutex;
36use std::time::Duration;
37
38use nix::sys::socket::AddressFamily;
39use nix::sys::socket::Backlog;
40use nix::sys::socket::Shutdown;
41use nix::sys::socket::SockFlag;
42use nix::sys::socket::SockType;
43use nix::sys::socket::SockaddrIn;
44use nix::sys::socket::sockopt::ReuseAddr;
45use reverie_ptrace::GdbConnection;
46use tokio::net::TcpListener;
47use tokio::net::TcpStream;
48use tokio::net::UnixStream;
49
50use crate::Context;
51use crate::Error;
52
53/// How often the relay tries the gdbserver's socket until the gdbserver
54/// listens on it, which it does once the guest is spawned.
55const GDBSERVER_RETRY: Duration = Duration::from_millis(5);
56
57/// A loopback TCP port bound, not yet listening, in the network namespace of
58/// the process that bound it, for a gdbserver to serve its client on.
59#[derive(Debug)]
60pub struct GdbPort {
61    socket: OwnedFd,
62    port: u16,
63}
64
65/// The port [`GdbPort::serve_next_gdbserver`] gave this process.
66static NEXT_GDBSERVER_PORT: Mutex<Option<GdbPort>> = Mutex::new(None);
67
68impl GdbPort {
69    /// Binds `127.0.0.1:PORT` in this process's network namespace; port 0
70    /// lets the system pick a free one, which [`GdbPort::port`] then names.
71    /// The socket is close-on-exec, so no guest inherits it. It listens only
72    /// when the tracer is about to spawn the guest (`connection`), so a
73    /// client finds the port listening once the gdbserver is moments from
74    /// taking it, as when the gdbserver bound the port itself.
75    pub fn bind(port: u16) -> Result<Self, Error> {
76        let socket = nix::sys::socket::socket(
77            AddressFamily::Inet,
78            SockType::Stream,
79            SockFlag::SOCK_CLOEXEC,
80            None,
81        )
82        .context("Failed to create the gdbserver's TCP socket")?;
83        // As std's TcpListener::bind, which the gdbserver used: a port whose
84        // last session is still in TIME_WAIT can be bound again at once.
85        nix::sys::socket::setsockopt(&socket, ReuseAddr, &true)
86            .context("Failed to set SO_REUSEADDR on the gdbserver's TCP socket")?;
87        nix::sys::socket::bind(socket.as_raw_fd(), &SockaddrIn::new(127, 0, 0, 1, port))
88            .with_context(|| format!("Failed to bind 127.0.0.1:{port} for the gdbserver"))?;
89        let port = nix::sys::socket::getsockname::<SockaddrIn>(socket.as_raw_fd())
90            .context("Failed to read the gdbserver's bound port")?
91            .port();
92        Ok(Self { socket, port })
93    }
94
95    /// The bound port.
96    pub fn port(&self) -> u16 {
97        self.port
98    }
99
100    /// Makes this the port that the next gdbserver this process starts
101    /// serves its client on. Call it in the process that starts the tracer,
102    /// the container's, after the port was bound outside the container.
103    pub fn serve_next_gdbserver(self) {
104        *NEXT_GDBSERVER_PORT
105            .lock()
106            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(self);
107    }
108
109    /// Listens on this port and starts the relay from it to the gdbserver on
110    /// the current Tokio runtime. Returns the abstract Unix socket the
111    /// gdbserver must listen on, or the error that left the port unusable,
112    /// before any guest exists.
113    fn relay(self) -> Result<GdbConnection, Error> {
114        // With SO_REUSEADDR, another socket may have bound the port too and
115        // listened first; then this fails, and the run with it.
116        nix::sys::socket::listen(&self.socket, Backlog::new(1)?).with_context(|| {
117            format!(
118                "The gdbserver cannot listen on 127.0.0.1:{}, which another process took after \
119                 Hermit bound it; pass another --gdbserver-port",
120                self.port
121            )
122        })?;
123        // Unique wherever the namespace is shared: this process holds the
124        // listening port.
125        let mut path = vec![0];
126        path.extend(format!("hermit-gdbserver-{}", self.port).into_bytes());
127        let path = PathBuf::from(OsString::from_vec(path));
128        let listener = std::net::TcpListener::from(self.socket);
129        listener.set_nonblocking(true)?;
130        let listener = TcpListener::from_std(listener)?;
131        tokio::spawn(serve_one_client(listener, self.port, path.clone()));
132        Ok(GdbConnection::Path(path))
133    }
134}
135
136/// Where a tracer's gdbserver takes its client, for a tracer configured to
137/// serve gdb on PORT: through a relay from the port
138/// [`GdbPort::serve_next_gdbserver`] gave this process, if one did
139/// ([`GdbPort::relay`]), or else on PORT, bound by the gdbserver in this
140/// process's network namespace. Only library callers that bound no port take
141/// the second way; a host gdb client reaches it only where that namespace is
142/// the host's.
143///
144/// Call it on the tracer's Tokio runtime, which runs the relay.
145pub(crate) fn connection(port: u16) -> Result<GdbConnection, Error> {
146    let served = NEXT_GDBSERVER_PORT
147        .lock()
148        .unwrap_or_else(|poisoned| poisoned.into_inner())
149        .take();
150    match served {
151        Some(bound) => bound.relay(),
152        None => Ok(port.into()),
153    }
154}
155
156/// Relays the one client LISTENER accepts to the gdbserver on its abstract
157/// Unix socket GDBSERVER, until either side ends the session.
158async fn serve_one_client(listener: TcpListener, port: u16, gdbserver: PathBuf) {
159    let (mut client, mut server) = match accept_client(listener, &gdbserver).await {
160        Ok(connected) => connected,
161        Err(error) => {
162            tracing::error!("The gdbserver relay on 127.0.0.1:{port} failed: {error:#}");
163            return;
164        }
165    };
166    // A session gdb ends by killing the guest can end with a reset; only the
167    // session is over, as when the client was connected directly.
168    if let Err(error) = tokio::io::copy_bidirectional(&mut client, &mut server).await {
169        tracing::debug!("The gdbserver session on 127.0.0.1:{port} ended: {error}");
170    }
171}
172
173/// Connects to the gdbserver once it listens, then accepts one client. A
174/// client that connects first waits in the kernel's accept queue meanwhile.
175async fn accept_client(
176    listener: TcpListener,
177    gdbserver: &Path,
178) -> Result<(TcpStream, UnixStream), Error> {
179    let server = loop {
180        match UnixStream::connect(gdbserver).await {
181            Ok(server) => break server,
182            Err(error) if error.kind() == io::ErrorKind::ConnectionRefused => {
183                tokio::time::sleep(GDBSERVER_RETRY).await;
184            }
185            Err(error) => return Err(error).context("Failed to connect to the gdbserver"),
186        }
187    };
188    let (client, address) = listener.accept().await?;
189    tracing::info!("Relaying gdb client {address} to the gdbserver");
190    // The gdbserver serves one client. The process that bound the port keeps
191    // its descriptor of this socket until the run ends, so dropping this one
192    // would leave the port listening for clients nothing accepts. Shutting it
193    // down for reading stops it listening for every holder, and refuses later
194    // clients: Linux's inet_shutdown disconnects a TCP_LISTEN socket on
195    // SHUT_RD (net/ipv4/af_inet.c:930-935 at v6.16), and tcp_disconnect stops
196    // the listen (net/ipv4/tcp.c:3339-3340).
197    if let Err(error) = nix::sys::socket::shutdown(listener.as_raw_fd(), Shutdown::Read) {
198        tracing::warn!(
199            "The gdbserver port still listens after its one client connected; a later client \
200             will wait unanswered: {error}"
201        );
202    }
203    Ok((client, server))
204}
205
206#[cfg(test)]
207mod tests {
208    use std::io::Read;
209    use std::io::Write;
210
211    use super::*;
212
213    /// Takes the one client of the gdbserver's abstract socket `path` and
214    /// echoes what it sends, upper-cased, until it closes.
215    fn fake_gdbserver(path: &Path) -> tokio::task::JoinHandle<()> {
216        use tokio::io::AsyncReadExt;
217        use tokio::io::AsyncWriteExt;
218
219        let listener = tokio::net::UnixListener::bind(path).unwrap();
220        tokio::spawn(async move {
221            let (mut stream, _) = listener.accept().await.unwrap();
222            drop(listener);
223            let mut buffer = [0; 64];
224            loop {
225                let read = stream.read(&mut buffer).await.unwrap();
226                if read == 0 {
227                    break;
228                }
229                stream
230                    .write_all(&buffer[..read].to_ascii_uppercase())
231                    .await
232                    .unwrap();
233            }
234        })
235    }
236
237    fn listening(port: u16) -> bool {
238        let hex = format!(":{port:04X} ");
239        std::fs::read_to_string("/proc/net/tcp")
240            .unwrap()
241            .lines()
242            .any(|line| line.contains(&hex) && line.split_whitespace().nth(3) == Some("0A"))
243    }
244
245    fn runtime() -> tokio::runtime::Runtime {
246        tokio::runtime::Builder::new_current_thread()
247            .enable_all()
248            .build()
249            .unwrap()
250    }
251
252    #[test]
253    fn a_bound_port_is_closed_on_exec_and_not_yet_listening() {
254        let port = GdbPort::bind(0).unwrap();
255        assert_ne!(port.port(), 0);
256        let flags = nix::fcntl::fcntl(&port.socket, nix::fcntl::FcntlArg::F_GETFD).unwrap();
257        assert_ne!(flags & libc::FD_CLOEXEC, 0);
258        // Bound, not listening: a client is refused until the tracer is about
259        // to spawn the guest, as when the gdbserver bound the port itself.
260        assert!(!listening(port.port()));
261        let error = std::net::TcpStream::connect(("127.0.0.1", port.port())).unwrap_err();
262        assert_eq!(error.kind(), io::ErrorKind::ConnectionRefused);
263    }
264
265    #[test]
266    fn a_port_in_use_is_refused_naming_it() {
267        let taken = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
268        let port = taken.local_addr().unwrap().port();
269        let error = format!("{:#}", GdbPort::bind(port).unwrap_err());
270        assert!(
271            error.contains(&format!("127.0.0.1:{port}")) && error.contains("EADDRINUSE"),
272            "{error}"
273        );
274    }
275
276    /// SO_REUSEADDR lets another socket bind the port while it does not
277    /// listen yet. If that one listens first, the relay fails before the
278    /// tracer spawns anything, naming the remedy, instead of leaving a guest
279    /// waiting for a client that cannot reach it.
280    #[test]
281    fn a_port_another_socket_listened_on_first_fails_before_the_guest_spawns() {
282        let runtime = runtime();
283        let _guard = runtime.enter();
284        let port = GdbPort::bind(0).unwrap();
285        let other = std::net::TcpListener::bind(("127.0.0.1", port.port())).unwrap();
286        let Err(error) = port.relay() else {
287            panic!("the relay listened on a port another socket listens on");
288        };
289        let error = format!("{error:#}");
290        assert!(
291            error.contains("--gdbserver-port") && error.contains("EADDRINUSE"),
292            "{error}"
293        );
294        drop(other);
295    }
296
297    /// The only test that touches the process-wide slot, so tests running
298    /// as threads of one process cannot take each other's port.
299    #[test]
300    fn a_served_port_is_relayed_once_and_then_the_gdbserver_binds_its_own() {
301        let runtime = runtime();
302        let _guard = runtime.enter();
303        let port = GdbPort::bind(0).unwrap();
304        let number = port.port();
305        port.serve_next_gdbserver();
306        match connection(4321).unwrap() {
307            GdbConnection::Path(path) => assert_eq!(
308                path.as_os_str().as_encoded_bytes(),
309                format!("\0hermit-gdbserver-{number}").as_bytes()
310            ),
311            GdbConnection::Addr(address) => panic!("{address} bound with a served port"),
312        }
313        match connection(4321).unwrap() {
314            GdbConnection::Addr(address) => assert_eq!(address.to_string(), "127.0.0.1:4321"),
315            GdbConnection::Path(path) => panic!("relayed to {path:?} with no served port"),
316        }
317    }
318
319    #[test]
320    fn the_relay_serves_one_client_once_the_gdbserver_listens() {
321        if crate::run_in_fresh_process(
322            "gdbserver::tests::the_relay_serves_one_client_once_the_gdbserver_listens",
323        ) {
324            return;
325        }
326        let runtime = runtime();
327        let port = GdbPort::bind(0).unwrap();
328        let number = port.port();
329        // The process that bound the port keeps its descriptor of the socket
330        // until the run ends.
331        let binders_copy = port.socket.try_clone().unwrap();
332        runtime.block_on(async move {
333            let GdbConnection::Path(path) = port.relay().unwrap() else {
334                panic!("a relay serves the gdbserver on a Unix socket");
335            };
336            assert!(listening(number));
337            // The client connects and sends before the gdbserver exists, as
338            // a client may while the tracer spawns the guest.
339            let client = tokio::task::spawn_blocking(move || {
340                let mut client = std::net::TcpStream::connect(("127.0.0.1", number)).unwrap();
341                client.write_all(b"$qsupported#00").unwrap();
342                let mut reply = [0; 14];
343                client.read_exact(&mut reply).unwrap();
344                // One client only: a second is refused.
345                let second = std::net::TcpStream::connect(("127.0.0.1", number)).unwrap_err();
346                drop(client);
347                (reply, second.kind())
348            });
349            tokio::time::sleep(GDBSERVER_RETRY * 4).await;
350            let server = fake_gdbserver(&path);
351            let (reply, second) = client.await.unwrap();
352            assert_eq!(&reply, b"$QSUPPORTED#00");
353            assert_eq!(second, io::ErrorKind::ConnectionRefused);
354            assert!(!listening(number));
355            // The client's close reaches the gdbserver, which ends.
356            server.await.unwrap();
357        });
358        drop(binders_copy);
359    }
360}