407 lines
10 KiB
C
407 lines
10 KiB
C
|
/*-
|
||
|
* SPDX-License-Identifier: BSD-2-Clause-FreeBSD
|
||
|
*
|
||
|
* Copyright (c) 2019 Isilon Systems, LLC.
|
||
|
*
|
||
|
* Redistribution and use in source and binary forms, with or without
|
||
|
* modification, are permitted provided that the following conditions
|
||
|
* are met:
|
||
|
* 1. Redistributions of source code must retain the above copyright
|
||
|
* notice, this list of conditions and the following disclaimer.
|
||
|
* 2. Redistributions in binary form must reproduce the above copyright
|
||
|
* notice, this list of conditions and the following disclaimer in the
|
||
|
* documentation and/or other materials provided with the distribution.
|
||
|
*
|
||
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||
|
* SUCH DAMAGE.
|
||
|
*/
|
||
|
|
||
|
/*
|
||
|
* netgdb.c
|
||
|
* FreeBSD subsystem supporting debugging the FreeBSD kernel over the network.
|
||
|
*
|
||
|
* There are three pieces necessary to use NetGDB.
|
||
|
*
|
||
|
* First, a dedicated proxy server must be running to accept connections from
|
||
|
* both NetGDB and gdb(1), and pass bidirectional traffic between the two
|
||
|
* protocols.
|
||
|
*
|
||
|
* Second, The NetGDB client is activated much like ordinary 'gdb' and
|
||
|
* similarly to 'netdump' in ddb(4). Like other debugnet(4) clients
|
||
|
* (netdump(4)), the network interface on the route to the proxy server must be
|
||
|
* online and support debugnet(4).
|
||
|
*
|
||
|
* Finally, the remote (k)gdb(1) uses 'target remote <proxy>:<port>' to connect
|
||
|
* to the proxy server.
|
||
|
*
|
||
|
* NetGDBv1 speaks the literal GDB remote serial protocol, and uses a 1:1
|
||
|
* relationship between GDB packets and plain debugnet packets. There is no
|
||
|
* encryption utilized to keep debugging sessions private, so this is only
|
||
|
* appropriate for local segments or trusted networks.
|
||
|
*/
|
||
|
|
||
|
#include <sys/cdefs.h>
|
||
|
__FBSDID("$FreeBSD$");
|
||
|
|
||
|
#include "opt_ddb.h"
|
||
|
#ifndef DDB
|
||
|
#error "NetGDB cannot be used without DDB at this time"
|
||
|
#endif
|
||
|
|
||
|
#include <sys/param.h>
|
||
|
#include <sys/kdb.h>
|
||
|
#include <sys/sbuf.h>
|
||
|
#include <sys/socket.h>
|
||
|
#include <sys/sysctl.h>
|
||
|
#include <sys/ttydefaults.h>
|
||
|
|
||
|
#include <machine/gdb_machdep.h>
|
||
|
|
||
|
#ifdef DDB
|
||
|
#include <ddb/ddb.h>
|
||
|
#include <ddb/db_command.h>
|
||
|
#include <ddb/db_lex.h>
|
||
|
#endif
|
||
|
|
||
|
#include <net/debugnet.h>
|
||
|
#include <net/if.h>
|
||
|
#include <net/if_var.h>
|
||
|
#include <net/route.h>
|
||
|
|
||
|
#include <gdb/gdb.h>
|
||
|
#include <gdb/gdb_int.h>
|
||
|
#include <gdb/netgdb.h>
|
||
|
|
||
|
FEATURE(netgdb, "NetGDB support");
|
||
|
SYSCTL_NODE(_debug_gdb, OID_AUTO, netgdb, CTLFLAG_RD, NULL,
|
||
|
"NetGDB parameters");
|
||
|
|
||
|
static unsigned netgdb_debug;
|
||
|
SYSCTL_UINT(_debug_gdb_netgdb, OID_AUTO, debug, CTLFLAG_RWTUN,
|
||
|
&netgdb_debug, 0,
|
||
|
"Debug message verbosity (0: off; 1: on)");
|
||
|
|
||
|
#define NETGDB_DEBUG(f, ...) do { \
|
||
|
if (netgdb_debug > 0) \
|
||
|
printf(("%s [%s:%d]: " f), __func__, __FILE__, __LINE__, ## \
|
||
|
__VA_ARGS__); \
|
||
|
} while (false)
|
||
|
|
||
|
static void netgdb_fini(void);
|
||
|
|
||
|
/* Runtime state. */
|
||
|
static char netgdb_rxbuf[GDB_BUFSZ + 16]; /* Some overhead for framing. */
|
||
|
static struct sbuf netgdb_rxsb;
|
||
|
static ssize_t netgdb_rx_off;
|
||
|
|
||
|
static struct debugnet_pcb *netgdb_conn;
|
||
|
static struct gdb_dbgport *netgdb_prev_dbgport;
|
||
|
static int *netgdb_prev_kdb_inactive;
|
||
|
|
||
|
/* TODO(CEM) disable ack mode */
|
||
|
|
||
|
/*
|
||
|
* Receive non-TX ACK packets on the client port.
|
||
|
*
|
||
|
* The mbuf chain will have all non-debugnet framing headers removed
|
||
|
* (ethernet, inet, udp). It will start with a debugnet_msg_hdr, of
|
||
|
* which the header is guaranteed to be contiguous. If m_pullup is
|
||
|
* used, the supplied in-out mbuf pointer should be updated
|
||
|
* appropriately.
|
||
|
*
|
||
|
* If the handler frees the mbuf chain, it should set the mbuf pointer
|
||
|
* to NULL. Otherwise, the debugnet input framework will free the
|
||
|
* chain.
|
||
|
*/
|
||
|
static void
|
||
|
netgdb_rx(struct debugnet_pcb *pcb, struct mbuf **mb)
|
||
|
{
|
||
|
const struct debugnet_msg_hdr *dnh;
|
||
|
struct mbuf *m;
|
||
|
uint32_t rlen, count;
|
||
|
int error;
|
||
|
|
||
|
m = *mb;
|
||
|
dnh = mtod(m, const void *);
|
||
|
|
||
|
if (ntohl(dnh->mh_type) == DEBUGNET_FINISHED) {
|
||
|
sbuf_putc(&netgdb_rxsb, CTRL('C'));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (ntohl(dnh->mh_type) != DEBUGNET_DATA) {
|
||
|
printf("%s: Got unexpected debugnet message %u\n",
|
||
|
__func__, ntohl(dnh->mh_type));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
rlen = ntohl(dnh->mh_len);
|
||
|
#define _SBUF_FREESPACE(s) ((s)->s_size - ((s)->s_len + 1))
|
||
|
if (_SBUF_FREESPACE(&netgdb_rxsb) < rlen) {
|
||
|
NETGDB_DEBUG("Backpressure: Not ACKing RX of packet that "
|
||
|
"would overflow our buffer (%zd/%zd used).\n",
|
||
|
netgdb_rxsb.s_len, netgdb_rxsb.s_size);
|
||
|
return;
|
||
|
}
|
||
|
#undef _SBUF_FREESPACE
|
||
|
|
||
|
error = debugnet_ack_output(pcb, dnh->mh_seqno);
|
||
|
if (error != 0) {
|
||
|
printf("%s: Couldn't ACK rx packet %u; %d\n", __func__,
|
||
|
ntohl(dnh->mh_seqno), error);
|
||
|
/*
|
||
|
* Sender will re-xmit, and assuming the condition is
|
||
|
* transient, we'll process the packet's contentss later.
|
||
|
*/
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
m_adj(m, sizeof(*dnh));
|
||
|
dnh = NULL;
|
||
|
|
||
|
/*
|
||
|
* Inlined m_apply -- why isn't there a macro or inline function
|
||
|
* version?
|
||
|
*/
|
||
|
while (m != NULL && m->m_len == 0)
|
||
|
m = m->m_next;
|
||
|
while (rlen > 0) {
|
||
|
MPASS(m != NULL && m->m_len >= 0);
|
||
|
count = min((uint32_t)m->m_len, rlen);
|
||
|
(void)sbuf_bcat(&netgdb_rxsb, mtod(m, const void *), count);
|
||
|
rlen -= count;
|
||
|
m = m->m_next;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* The following routines implement a pseudo GDB debugport (an emulated serial
|
||
|
* driver that the MI gdb(4) code does I/O with).
|
||
|
*/
|
||
|
|
||
|
static int
|
||
|
netgdb_dbg_getc(void)
|
||
|
{
|
||
|
int c;
|
||
|
|
||
|
while (true) {
|
||
|
/* Pull bytes off any currently cached packet first. */
|
||
|
if (netgdb_rx_off < sbuf_len(&netgdb_rxsb)) {
|
||
|
c = netgdb_rxsb.s_buf[netgdb_rx_off];
|
||
|
netgdb_rx_off++;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
/* Reached EOF? Reuse buffer. */
|
||
|
sbuf_clear(&netgdb_rxsb);
|
||
|
netgdb_rx_off = 0;
|
||
|
|
||
|
/* Check for CTRL-C on console/serial, if any. */
|
||
|
if (netgdb_prev_dbgport != NULL) {
|
||
|
c = netgdb_prev_dbgport->gdb_getc();
|
||
|
if (c == CTRL('C'))
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
debugnet_network_poll(netgdb_conn);
|
||
|
}
|
||
|
|
||
|
if (c == CTRL('C')) {
|
||
|
netgdb_fini();
|
||
|
/* Caller gdb_getc() will print that we got ^C. */
|
||
|
}
|
||
|
return (c);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
netgdb_dbg_sendpacket(const void *buf, size_t len)
|
||
|
{
|
||
|
struct debugnet_proto_aux aux;
|
||
|
int error;
|
||
|
|
||
|
MPASS(len <= UINT32_MAX);
|
||
|
|
||
|
/*
|
||
|
* GDB packet boundaries matter. debugnet_send() fragments a single
|
||
|
* request into many sequential debugnet messages. Mark full packet
|
||
|
* length and offset for potential reassembly by the proxy.
|
||
|
*/
|
||
|
aux = (struct debugnet_proto_aux) {
|
||
|
.dp_aux2 = len,
|
||
|
};
|
||
|
|
||
|
error = debugnet_send(netgdb_conn, DEBUGNET_DATA, buf, len, &aux);
|
||
|
if (error != 0) {
|
||
|
printf("%s: Network error: %d; trying to switch back to ddb.\n",
|
||
|
__func__, error);
|
||
|
netgdb_fini();
|
||
|
|
||
|
if (kdb_dbbe_select("ddb") != 0)
|
||
|
printf("The ddb backend could not be selected.\n");
|
||
|
else {
|
||
|
printf("using longjmp, hope it works!\n");
|
||
|
kdb_reenter();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
/* Just used for + / - GDB-level ACKs. */
|
||
|
static void
|
||
|
netgdb_dbg_putc(int i)
|
||
|
{
|
||
|
char c;
|
||
|
|
||
|
c = i;
|
||
|
netgdb_dbg_sendpacket(&c, 1);
|
||
|
|
||
|
}
|
||
|
|
||
|
static struct gdb_dbgport netgdb_gdb_dbgport = {
|
||
|
.gdb_name = "netgdb",
|
||
|
.gdb_getc = netgdb_dbg_getc,
|
||
|
.gdb_putc = netgdb_dbg_putc,
|
||
|
.gdb_term = netgdb_fini,
|
||
|
.gdb_sendpacket = netgdb_dbg_sendpacket,
|
||
|
.gdb_dbfeatures = GDB_DBGP_FEAT_WANTTERM,
|
||
|
};
|
||
|
|
||
|
static void
|
||
|
netgdb_init(void)
|
||
|
{
|
||
|
struct kdb_dbbe *be, **iter;
|
||
|
|
||
|
/*
|
||
|
* Force enable GDB. (If no other debugports were registered at boot,
|
||
|
* KDB thinks it doesn't exist.)
|
||
|
*/
|
||
|
SET_FOREACH(iter, kdb_dbbe_set) {
|
||
|
be = *iter;
|
||
|
if (strcmp(be->dbbe_name, "gdb") != 0)
|
||
|
continue;
|
||
|
if (be->dbbe_active == -1) {
|
||
|
netgdb_prev_kdb_inactive = &be->dbbe_active;
|
||
|
be->dbbe_active = 0;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
/* Force netgdb debugport. */
|
||
|
netgdb_prev_dbgport = gdb_cur;
|
||
|
gdb_cur = &netgdb_gdb_dbgport;
|
||
|
|
||
|
sbuf_new(&netgdb_rxsb, netgdb_rxbuf, sizeof(netgdb_rxbuf),
|
||
|
SBUF_FIXEDLEN);
|
||
|
netgdb_rx_off = 0;
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
netgdb_fini(void)
|
||
|
{
|
||
|
|
||
|
/* TODO: tear down conn gracefully? */
|
||
|
if (netgdb_conn != NULL) {
|
||
|
debugnet_free(netgdb_conn);
|
||
|
netgdb_conn = NULL;
|
||
|
}
|
||
|
|
||
|
sbuf_delete(&netgdb_rxsb);
|
||
|
|
||
|
gdb_cur = netgdb_prev_dbgport;
|
||
|
|
||
|
if (netgdb_prev_kdb_inactive != NULL) {
|
||
|
*netgdb_prev_kdb_inactive = -1;
|
||
|
netgdb_prev_kdb_inactive = NULL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#ifdef DDB
|
||
|
/*
|
||
|
* Usage: netgdb -s <server> [-g <gateway -c <localip> -i <interface>]
|
||
|
*
|
||
|
* Order is not significant.
|
||
|
*
|
||
|
* Currently, this command does not support configuring encryption or
|
||
|
* compression.
|
||
|
*/
|
||
|
DB_FUNC(netgdb, db_netgdb_cmd, db_cmd_table, CS_OWN, NULL)
|
||
|
{
|
||
|
struct debugnet_ddb_config params;
|
||
|
struct debugnet_conn_params dcp;
|
||
|
struct debugnet_pcb *pcb;
|
||
|
int error;
|
||
|
|
||
|
if (panicstr == NULL) {
|
||
|
/* TODO: This limitation should be removed in future work. */
|
||
|
printf("%s: netgdb is currently limited to use only after a "
|
||
|
"panic. Sorry.\n", __func__);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
error = debugnet_parse_ddb_cmd("netgdb", ¶ms);
|
||
|
if (error != 0) {
|
||
|
db_printf("Error configuring netgdb: %d\n", error);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Must initialize netgdb_rxsb before debugnet_connect(), because we
|
||
|
* might be getting rx handler callbacks from the send->poll path
|
||
|
* during debugnet_connect().
|
||
|
*/
|
||
|
netgdb_init();
|
||
|
|
||
|
if (!params.dd_has_client)
|
||
|
params.dd_client = INADDR_ANY;
|
||
|
if (!params.dd_has_gateway)
|
||
|
params.dd_gateway = INADDR_ANY;
|
||
|
|
||
|
dcp = (struct debugnet_conn_params) {
|
||
|
.dc_ifp = params.dd_ifp,
|
||
|
.dc_client = params.dd_client,
|
||
|
.dc_server = params.dd_server,
|
||
|
.dc_gateway = params.dd_gateway,
|
||
|
.dc_herald_port = NETGDB_HERALDPORT,
|
||
|
.dc_client_port = NETGDB_CLIENTPORT,
|
||
|
.dc_herald_aux2 = NETGDB_PROTO_V1,
|
||
|
.dc_rx_handler = netgdb_rx,
|
||
|
};
|
||
|
|
||
|
error = debugnet_connect(&dcp, &pcb);
|
||
|
if (error != 0) {
|
||
|
printf("failed to contact netgdb server: %d\n", error);
|
||
|
netgdb_fini();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
netgdb_conn = pcb;
|
||
|
|
||
|
if (kdb_dbbe_select("gdb") != 0) {
|
||
|
db_printf("The remote GDB backend could not be selected.\n");
|
||
|
netgdb_fini();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Mark that we are done in ddb(4). Return -> kdb_trap() should
|
||
|
* re-enter with the new backend.
|
||
|
*/
|
||
|
db_cmd_loop_done = 1;
|
||
|
gdb_return_to_ddb = true;
|
||
|
db_printf("(detaching GDB will return control to DDB)\n");
|
||
|
#if 0
|
||
|
/* Aspirational, but does not work reliably. */
|
||
|
db_printf("(ctrl-c will return control to ddb)\n");
|
||
|
#endif
|
||
|
}
|
||
|
#endif /* DDB */
|