Linux GPIO subsystem development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Michael Zaidman" <michael.zaidman@gmail.com>
Cc: linux-input@vger.kernel.org, linux-serial@vger.kernel.org,
	linux-gpio@vger.kernel.org, dmitry.torokhov@gmail.com
Subject: Re: [PATCH v2 01/15] HID: ft260: add serial driver
Date: Fri, 30 Oct 2026 23:00:22 +0000	[thread overview]
Message-ID: <sashiko-outbox-191611@kernel.org> (raw)
In-Reply-To: <20261030223637.86907-2-michael.zaidman@gmail.com>

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] HID: ft260: broken minor index allocation and bounds check in ft260_uart_add_port()
- [High] HID: ft260: use-after-free of hdev and xmit_fifo across ft260_remove()
- [High] HID: ft260: unsynchronized transmit loop and infinite loop in ft260_uart_transmit_chars()
- [High] HID: ft260: use-after-free and double free in ft260_probe() on FT260_IFACE_NONE
- [High] HID: ft260: unguarded UART report in ft260_raw_event() on I2C interface
- [High] HID: ft260: sleeping in atomic context in ft260_uart_write()
- [Medium] HID: ft260: race in ft260_uart_probe() registering tty before hw config
--

commit bd086b64065aa177dfdec81fc03e141758ca2307
Author: Christina Quast <contact@christina-quast.de>

HID: ft260: add serial driver

This commit adds a TTY serial driver (/dev/ttyFTx) for the UART interface
of FTDI FT260 USB HID devices, supporting termios configuration and serial
data transfers alongside the existing I2C interface support.

> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 64c9c940db46e..982e2ca7a2de6 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c

[ ... ]

> @@ -1001,51 +1101,478 @@ static const struct attribute_group ft260_attr_group = {
>  	}
>  };
>  
> -static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
> +/***
> + * START Serial dev part
> + */
> +static DEFINE_MUTEX(ft260_uart_list_lock);
> +static LIST_HEAD(ft260_uart_device_list);
> +
> +static struct ft260_device *ft260_dev_by_index(int index)
> +{
> +	struct ft260_device *port;
> +
> +	list_for_each_entry(port, &ft260_uart_device_list, device_list) {
> +		if (index == port->index)
> +			return port;
> +	}
> +	return NULL;
> +}
> +
> +static int ft260_uart_add_port(struct ft260_device *port)
>  {
> +	int index = 0, ret = 0;
>  	struct ft260_device *dev;
> -	struct ft260_get_chip_version_report version;
> +
> +	spin_lock_init(&port->write_lock);
> +	if (kfifo_alloc(&port->xmit_fifo, FIFO_SIZE, GFP_KERNEL))
> +		return -ENOMEM;
> +
> +	mutex_lock(&ft260_uart_list_lock);
> +	list_for_each_entry(dev, &ft260_uart_device_list, device_list) {
> +		if (dev->index != index)
> +			break;
> +		index++;
> +	}
> +
> +	port->index = index;
> +	list_add_tail(&port->device_list, &ft260_uart_device_list);
> +	mutex_unlock(&ft260_uart_list_lock);
> +
> +	return ret;
> +}

[Severity: High]
As reported by Sashiko on v1 [1], can ft260_uart_add_port() in
drivers/hid/hid-ft260.c assign duplicate minor indices or exceed
UART_COUNT_MAX?

The loop in ft260_uart_add_port() searches for the lowest unused index by
breaking as soon as dev->index != index, which assumes ft260_uart_device_list
is kept sorted in ascending order of index. However, the new port is always
appended at the end of the list with list_add_tail(&port->device_list,
&ft260_uart_device_list) instead of being inserted before dev
(&dev->device_list).

If port 0 and port 1 are probed (list: [0, 1]), port 0 is removed (list:
[1]), and a third UART device is probed, it gets index 0 and is appended to
the tail (list: [1, 0]). When a fourth UART device is probed, the loop sees
dev->index == 1 != 0 on the first entry, breaks immediately, and assigns a
duplicate index 0.

In ft260_uart_probe(), tty_port_register_device_attr() then calls
tty_port_link_device(), which overwrites ft260_tty_driver->ports[0] with the
new port before device_register() fails with -EEXIST. The error path in
ft260_probe() frees the new port via tty_port_put(&dev->port), leaving
ft260_tty_driver->ports[0] as a dangling pointer to freed memory while the
first /dev/ttyFT0 remains registered.

In addition, ft260_uart_add_port() does not check whether index >=
UART_COUNT_MAX (4), so probing a fifth UART device passes index == 4 to
tty_port_register_device_attr() -> tty_port_link_device() and triggers
WARN_ON(index >= driver->num).

> +
> +static void ft260_uart_port_put(struct ft260_device *port)
> +{
> +	tty_port_put(&port->port);
> +}
> +
> +static void ft260_uart_port_remove(struct ft260_device *port)
> +{
> +	timer_delete_sync(&port->wakeup_timer);
> +
> +	mutex_lock(&ft260_uart_list_lock);
> +	list_del(&port->device_list);
> +	mutex_unlock(&ft260_uart_list_lock);
> +
> +	tty_port_tty_vhangup(&port->port);
> +
> +	spin_lock(&port->write_lock);
> +	kfifo_free(&port->xmit_fifo);
> +	spin_unlock(&port->write_lock);
> +}

[Severity: High]
As reported by Sashiko on v1 [2], does freeing port->xmit_fifo in
ft260_uart_port_remove() and stopping port->hdev in ft260_remove() cause a
use-after-free when TTY operations race with or follow device removal?

Although struct ft260_device lives until ft260_uart_port_destroy(),
ft260_uart_port_remove() frees port->xmit_fifo during ft260_remove(), and the
HID core frees port->hdev once ft260_remove() returns:

drivers/hid/hid-ft260.c:ft260_remove() {
	if (dev->ft260_is_serial) {
		timer_delete_sync(&dev->wakeup_timer);
		cancel_work_sync(&dev->wakeup_work);
		tty_port_unregister_device(&dev->port, ft260_tty_driver,
					   dev->index);
		ft260_uart_port_remove(dev);
	}
	...
	hid_hw_close(hdev);
	hid_hw_stop(hdev);
}

If tty_open() has completed ft260_uart_install() (obtaining a port reference
from ft260_uart_device_list and dropping tty_mutex) before
ft260_uart_port_remove() runs, but has not yet reached tty_port_tty_set() in
tty_port_open(), port->port.tty is still NULL when tty_port_tty_vhangup() is
called. When tty_open() continues into ft260_uart_open() -> tty_port_open()
-> ft260_uart_activate():

drivers/hid/hid-ft260.c:ft260_uart_activate() {
	...
	spin_lock(&port->write_lock);
	kfifo_reset(&port->xmit_fifo);
	spin_unlock(&port->write_lock);

	ft260_uart_change_speed(port, &tty->termios, NULL);
	clear_bit(TTY_IO_ERROR, &tty->flags);
}

it resets the freed xmit_fifo, calls ft260_uart_change_speed() on the
stopped/freed port->hdev, ignores the error return, clears TTY_IO_ERROR, and
lets subsequent writes and termios changes proceed on a disconnected device.

Even for an already-opened TTY, concurrent or subsequent calls to
ft260_uart_set_termios() (which does not check tty_io_error()) or in-flight
ft260_uart_write() / ft260_uart_transmit_chars() calls can dereference the
freed port->hdev and port->xmit_fifo without any disconnect check or
synchronization against ft260_remove(). Because __kfifo_free() sets
fifo->data = NULL and fifo->mask = 0, kfifo_unused() evaluates to 1 when
in == out, causing a racing kfifo_in_locked() in ft260_uart_write() to
memcpy() into a NULL fifo->data pointer.

Should kfifo_free(&port->xmit_fifo) be deferred to ft260_uart_port_destroy(),
with a disconnect flag and lock protecting all port->hdev accesses in the
TTY callbacks?

[ ... ]

> +static int ft260_uart_transmit_chars(struct ft260_device *port)
> +{
> +	struct hid_device *hdev = port->hdev;
> +	struct kfifo *xmit = &port->xmit_fifo;
> +	struct tty_struct *tty;
> +	struct ft260_uart_write_request_report *rep;
> +	int len, data_len, ret = 0;
> +
> +	tty = tty_port_tty_get(&port->port);
> +
> +	data_len = kfifo_len(xmit);
> +	if (!tty || !data_len) {
> +		ret = -EINVAL;
> +		goto tty_out;
> +	}
> +
> +	rep = (struct ft260_uart_write_request_report *)port->write_buf;
> +
> +	do {
> +		len = min(data_len, FT260_WR_DATA_MAX);
> +
> +		rep->report = FT260_UART_DATA_REPORT_ID(len);
> +		rep->length = len;
> +
> +		len = kfifo_out_locked(xmit, rep->data, len, &port->write_lock);
> +
> +		ret = ft260_hid_output_report(hdev, (u8 *)rep, len + sizeof(*rep));
> +		if (ret < 0) {
> +			hid_err(hdev, "Failed to start transfer, ret %d\n", ret);
> +			goto tty_out;
> +		}
> +
> +		data_len -= len;
> +		port->icount.tx += len;
> +	} while (data_len > 0);

[Severity: High]
As reported by Sashiko on v1 [3], what happens in ft260_uart_transmit_chars()
if two threads transmit concurrently, or if xmit_fifo is drained or reset
while ft260_hid_output_report() is blocking?

First, port->write_buf is shared across the port without any lock protecting
it across the blocking ft260_hid_output_report() call, so concurrent callers
(for example, normal write vs line discipline echo) will corrupt rep->report,
rep->length, and rep->data.

Second, rep->report and rep->length are computed from data_len before
kfifo_out_locked() dequeues the data. If another thread consumes bytes from
xmit_fifo (or ft260_uart_activate() resets the fifo, or
ft260_uart_port_remove() frees it), kfifo_out_locked() can return fewer bytes
than rep->length, including 0.

When kfifo_out_locked() returns len == 0:
1. rep->length still holds the stale non-zero length, violating the
   __counted_by(length) annotation on rep->data and sending a malformed
   2-byte report header with a non-zero length field.
2. If ft260_hid_output_report() succeeds, data_len -= 0 leaves data_len
   greater than 0, causing the do-while (data_len > 0) loop to spin
   indefinitely.

Also, if ft260_hid_output_report() fails mid-loop, the chunk already removed
by kfifo_out_locked() is lost and any remaining bytes stay in xmit_fifo
while ft260_uart_write() still returns the full queued count. Note that while
a subsequent patch in the series separates i2c_wr_buf and uart_wr_buf, the
unlocked uart_wr_buf use, pre-dequeue rep->length assignment, and infinite
loop when kfifo_out_spinlocked() returns 0 remain present.

[ ... ]

> +static ssize_t ft260_uart_write(struct tty_struct *tty, const u8 *buf,
> +				 size_t count)
> +{
> +	struct ft260_device *port = tty->driver_data;
> +	struct hid_device *hdev = port->hdev;
> +	int len, ret;
> +
> +	len = kfifo_in_locked(&port->xmit_fifo, buf, count, &port->write_lock);
> +	ft260_dbg("count: %zu, len: %d", count, len);
> +
> +	ret = ft260_uart_transmit_chars(port);
> +	if (ret < 0)
> +		hid_dbg(hdev, "Failed to transmit chars: %d\n", ret);
> +
> +	return len;
> +}

[Severity: High]
Can ft260_uart_write() sleep in atomic context here?

The struct tty_operations write() callback contract in
include/linux/tty_driver.h specifies that write() may not sleep, and standard
line disciplines (such as N_PPP, N_SLIP, N_GSM0710, N_SLCAN, and N_MCTP)
invoke tty->ops->write() while holding spinlocks or running in softirq
context.

However, ft260_uart_write() synchronously calls:

ft260_uart_transmit_chars()
  -> ft260_hid_output_report()
       -> kmemdup(..., GFP_KERNEL)
       -> hid_hw_output_report()

which performs a GFP_KERNEL allocation and blocks waiting for synchronous USB
URB completion. Should transmission from xmit_fifo be deferred to a
workqueue item instead of running synchronously in ft260_uart_write()?

[ ... ]

> +static int ft260_uart_probe(struct hid_device *hdev, struct ft260_device *dev)
> +{
> +	struct ft260_configure_uart_request req;
> +	int ret;
> +	struct device *devt;
> +
> +	INIT_WORK(&dev->wakeup_work, ft260_uart_do_wakeup);
> +	// FIXME: Do I need that if I have cancel_work_sync?
> +	// FIXME: are all kfifo access secured by lock? with irq or not?
> +	dev->reschedule_work = false;
> +	/* Work not started at this point */
> +	timer_setup(&dev->wakeup_timer, ft260_uart_start_wakeup, 0);
> +
> +	tty_port_init(&dev->port);
> +	dev->port.ops = &ft260_uart_port_ops;
> +
> +	ret = ft260_uart_add_port(dev);
> +	if (ret) {
> +		hid_err(hdev, "failed to add port\n");
> +		goto err_port;
> +	}
> +	devt = tty_port_register_device_attr(&dev->port,
> +					     ft260_tty_driver,
> +					     dev->index, &hdev->dev,
> +					     dev, NULL);
> +	if (IS_ERR(devt)) {
> +		hid_err(hdev, "failed to register tty port\n");
> +		ret = PTR_ERR(devt);
> +		goto err_register_tty;
> +	}
> +	hid_info(hdev, "Registering device /dev/%s%d\n",
> +		ft260_tty_driver->name, dev->index);
> +
> +	/* Send Feature Report to Configure FT260 as UART 9600-8-N-1 */
> +	req.report	= FT260_SYSTEM_SETTINGS;
> +	req.request	= FT260_SET_UART_CONFIG;
> +	req.flow_ctrl	= FT260_UART_CFG_FLOW_CTRL_NONE;
> +	put_unaligned_le32(9600, &req.baudrate);
> +	req.data_bit	= FT260_UART_CFG_DATA_BITS_8;
> +	req.parity	= FT260_UART_CFG_PAR_NO;
> +	req.stop_bit	= FT260_UART_CFG_STOP_ONE_BIT;
> +	req.breaking	= FT260_UART_CFG_BREAKING_NO;
> +
> +	ret = ft260_hid_feature_report_set(hdev, (u8 *)&req, sizeof(req));

[Severity: Medium]
In ft260_uart_probe(), the port is added to ft260_uart_device_list and
registered via tty_port_register_device_attr() before the initial 9600-8-N-1
FT260_SET_UART_CONFIG feature report is sent to the hardware.

If userspace (such as udev or ModemManager reacting to the KOBJ_ADD uevent)
opens /dev/ttyFTx and configures termios before ft260_hid_feature_report_set()
runs here, won't ft260_uart_probe() overwrite the userspace UART settings in
hardware with 9600-8-N-1? Should the initial hardware configuration happen
before publishing and registering the TTY device?

> +	if (ret < 0) {
> +		hid_err(hdev, "ft260_hid_feature_report_set failed: %d\n",
> +			ret);
> +		goto err_hid_report;
> +	}
> +
> +	return 0;
> +
> +err_hid_report:
> +	tty_port_unregister_device(&dev->port, ft260_tty_driver, dev->index);
> +err_register_tty:
> +	ft260_uart_port_remove(dev);
> +err_port:
> +	timer_delete_sync(&dev->wakeup_timer);
> +	return ret;
> +}
> +
> +static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
> +{
> +	struct ft260_device *dev;
> +	struct ft260_get_chip_version_report version;
> +	int ret;
> +
> +	if (!hid_is_usb(hdev))
> +		return -EINVAL;
> +
> +	/* We cannot used devm_kzalloc here, because port has to survive until
> +	 * destroy function call
> +	 */
> +	dev = kzalloc_obj(*dev, GFP_KERNEL);
> +	if (!dev) {
> +		ret = -ENOMEM;
> +		goto alloc_fail;
> +	}
> +	hid_set_drvdata(hdev, dev);
> +	mutex_init(&dev->lock);
> +	spin_lock_init(&dev->read_lock);
> +	init_completion(&dev->wait);
> +
> +	ret = hid_parse(hdev);
> +	if (ret) {
> +		hid_err(hdev, "failed to parse HID\n");
> +		goto hid_fail;
> +	}
> +
> +	ret = hid_hw_start(hdev, 0);
> +	if (ret) {
> +		hid_err(hdev, "failed to start HID HW\n");
> +		goto hid_fail;
> +	}
> +
> +	ret = hid_hw_open(hdev);
> +	if (ret) {
> +		hid_err(hdev, "failed to open HID HW\n");
> +		goto err_hid_stop;
> +	}
> +
> +	ret = ft260_hid_feature_report_get(hdev, FT260_CHIP_VERSION,
> +					   (u8 *)&version, sizeof(version));
> +	if (ret < 0) {
> +		hid_err(hdev, "failed to retrieve chip version\n");
> +		goto err_hid_close;
> +	}
> +
> +	hid_info(hdev, "chip code: %02x%02x %02x%02x\n",
> +		 version.chip_code[0], version.chip_code[1],
> +		 version.chip_code[2], version.chip_code[3]);
> +
> +	ret = ft260_get_interface_type(hdev, dev);
> +	if (ret <= FT260_IFACE_NONE)
> +		goto err_hid_close;

[Severity: High]
What happens in ft260_probe() if ft260_get_interface_type() returns
FT260_IFACE_NONE (0)?

In ft260_get_interface_type(), ret is initialized to the return value of
ft260_get_system_config(hdev, &cfg), which is 0 on success:

drivers/hid/hid-ft260.c:ft260_get_interface_type() {
	...
	ret = ft260_get_system_config(hdev, &cfg);
	if (ret < 0)
		return ret;
	...
	switch (cfg.chip_mode) {
	case FT260_MODE_ALL:
	case FT260_MODE_BOTH:
		...
		break;
	case FT260_MODE_UART:
		...
		break;
	case FT260_MODE_I2C:
		...
		break;
	}

	return ret;
}

If cfg.chip_mode does not match any of the switch cases,
ft260_get_interface_type() returns 0 (FT260_IFACE_NONE). In ft260_probe(),
the check if (ret <= FT260_IFACE_NONE) jumps to err_hid_close with ret == 0:

err_hid_close:
	hid_hw_close(hdev);
err_hid_stop:
	hid_hw_stop(hdev);
hid_fail:
	kfree(dev);
alloc_fail:
	return ret;

This frees dev while leaving hdev drvdata pointing to it, and returns 0
(probe success) to the HID core. When ft260_remove() is later called on
device unbind or disconnect, it retrieves the dangling dev pointer from
hid_get_drvdata(hdev), accesses freed memory, and calls kfree(dev) a second
time.

[ ... ]

> @@ -1157,9 +1835,17 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
>  		if (complete_read)
>  			complete(&dev->wait);
>  
> -	} else {
> -		hid_err(hdev, "unhandled report %#02x\n", xfer->report);
> +		return 0;
> +
> +	} else if (xfer->length > FT260_RD_DATA_MAX) {
> +		hid_err(hdev, "received data too long (%d)\n", xfer->length);
> +		return -EBADR;
> +	} else if (xfer->report >= FT260_UART_REPORT_MIN &&
> +		   xfer->report <= FT260_UART_REPORT_MAX) {
> +		return ft260_uart_receive_chars(dev, xfer->data, xfer->length);
>  	}

[Severity: High]
Can ft260_raw_event() dereference a NULL pointer if a UART report (ID
0xF0..0xFE) arrives on an I2C interface?

When an interface is probed as I2C (dev->ft260_is_serial is false),
ft260_uart_probe() is not called, so tty_port_init(&dev->port) never runs
and dev->port.buf.tail remains NULL. However, ft260_raw_event() dispatches
reports in FT260_UART_REPORT_MIN..FT260_UART_REPORT_MAX to
ft260_uart_receive_chars() without checking dev->ft260_is_serial:

ft260_raw_event()
  -> ft260_uart_receive_chars()
       -> tty_insert_flip_string(&port->port, data, length)
            -> __tty_insert_flip_string_flags()
                 -> __tty_buffer_request_room()

If a malformed or malicious USB device (or HID-BPF program) sends a UART
input report on the I2C interface, __tty_buffer_request_room() will
dereference NULL port->buf.tail. Should ft260_raw_event() verify
dev->ft260_is_serial before calling ft260_uart_receive_chars()?

[1] https://lore.kernel.org/r/20260822213941.98882-2-michael.zaidman@gmail.com
[2] https://lore.kernel.org/r/20260822215615.2D8541F000E9@smtp.kernel.org
[3] https://lore.kernel.org/r/20260822215615.2D8541F000E9@smtp.kernel.org

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261030223637.86907-1-michael.zaidman@gmail.com?part=1

  reply	other threads:[~2026-10-30 23:00 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 01/15] HID: ft260: add serial driver Michael Zaidman
2026-10-30 23:00   ` sashiko-bot [this message]
2026-10-30 22:36 ` [PATCH v2 02/15] HID: ft260: uart: fix the " Michael Zaidman
2026-10-30 23:03   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 03/15] HID: ft260: add GPIO support Michael Zaidman
2026-10-30 22:53   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 04/15] HID: ft260: i2c: reduce driver module loading time Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 05/15] HID: ft260: i2c: silence sysfs store big-numbers Michael Zaidman
2026-10-30 22:47   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 06/15] HID: ft260: i2c: reduce bus-error message severity Michael Zaidman
2026-10-30 22:44   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 07/15] HID: ft260: uart: enable flow control Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 08/15] HID: ft260: uart: add modem pins control via ioctl Michael Zaidman
2026-10-30 22:55   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 09/15] HID: ft260: group sysfs attrs per HID interface Michael Zaidman
2026-10-30 22:48   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 10/15] HID: ft260: i2c: fix large write transaction failure Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 11/15] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration Michael Zaidman
2026-10-30 22:59   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 12/15] HID: ft260: i2c: abort in-flight transfers with STOP before reset Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 13/15] HID: ft260: i2c: reject an I2C clock outside the supported range Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 14/15] HID: ft260: i2c: fix i2c_reset sysfs store return value Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 15/15] HID: ft260: i2c: recover a bus held by a slave Michael Zaidman
2026-10-30 22:59   ` sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-191611@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=dmitry.torokhov@gmail.com \
    --cc=linux-gpio@vger.kernel.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-serial@vger.kernel.org \
    --cc=michael.zaidman@gmail.com \
    --cc=sashiko-reviews@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox