All of lore.kernel.org
 help / color / mirror / Atom feed
From: Harald Freudenberger <freude@linux.ibm.com>
To: agk@redhat.com, snitzer@kernel.org, mpatocka@redhat.com,
	ifranzki@linux.ibm.com
Cc: linux-s390@vger.kernel.org, dm-devel@lists.linux.dev,
	herbert@gondor.apana.org.au, dengler@linux.ibm.com
Subject: [PATCH v1 1/1] dm-integrity: Implement asynch digest support
Date: Wed, 15 Jan 2025 17:46:57 +0100	[thread overview]
Message-ID: <20250115164657.84650-2-freude@linux.ibm.com> (raw)
In-Reply-To: <20250115164657.84650-1-freude@linux.ibm.com>

Use the async digest in-kernel crypto API instead of the
synchronous digest API. This has the advantage of being able
to use synchronous as well as asynchronous digest implementations
as the in-kernel API has an automatic wrapping mechanism
to provide all synchronous digests via the asynch API.

Tested with crc32, sha256, hmac-sha256 and the s390 specific
implementations for hmac-sha256 and protected key phmac-sha256.

Signed-off-by: Harald Freudenberger <freude@linux.ibm.com>
---
 drivers/md/dm-integrity.c | 220 ++++++++++++++++++++++++--------------
 1 file changed, 138 insertions(+), 82 deletions(-)

diff --git a/drivers/md/dm-integrity.c b/drivers/md/dm-integrity.c
index ee9f7cecd78e..1504db9276d1 100644
--- a/drivers/md/dm-integrity.c
+++ b/drivers/md/dm-integrity.c
@@ -195,7 +195,7 @@ struct dm_integrity_c {
 	struct scatterlist **journal_io_scatterlist;
 	struct skcipher_request **sk_requests;
 
-	struct crypto_shash *journal_mac;
+	struct crypto_ahash *journal_mac;
 
 	struct journal_node *journal_tree;
 	struct rb_root journal_tree_root;
@@ -221,7 +221,7 @@ struct dm_integrity_c {
 
 	int failed;
 
-	struct crypto_shash *internal_hash;
+	struct crypto_ahash *internal_hash;
 
 	struct dm_target *ti;
 
@@ -488,11 +488,14 @@ static void sb_set_version(struct dm_integrity_c *ic)
 
 static int sb_mac(struct dm_integrity_c *ic, bool wr)
 {
-	SHASH_DESC_ON_STACK(desc, ic->journal_mac);
-	int r;
-	unsigned int mac_size = crypto_shash_digestsize(ic->journal_mac);
+	unsigned int mac_size = crypto_ahash_digestsize(ic->journal_mac);
 	__u8 *sb = (__u8 *)ic->sb;
 	__u8 *mac = sb + (1 << SECTOR_SHIFT) - mac_size;
+	struct ahash_request *req;
+	DECLARE_CRYPTO_WAIT(wait);
+	struct scatterlist sg;
+	unsigned int nbytes;
+	int r;
 
 	if (sizeof(struct superblock) + mac_size > 1 << SECTOR_SHIFT ||
 	    mac_size > HASH_MAX_DIGESTSIZE) {
@@ -500,29 +503,44 @@ static int sb_mac(struct dm_integrity_c *ic, bool wr)
 		return -EINVAL;
 	}
 
-	desc->tfm = ic->journal_mac;
+	req = ahash_request_alloc(ic->journal_mac, GFP_KERNEL);
+	if (unlikely(!req)) {
+		dm_integrity_io_error(ic, "ahash_request_alloc", -ENOMEM);
+		return -ENOMEM;
+	}
+	ahash_request_set_callback(req, 0, crypto_req_done, &wait);
+
+	sg_init_table(&sg, 1);
+	nbytes = mac - sb;
+	sg_set_buf(&sg, sb, nbytes);
 
 	if (likely(wr)) {
-		r = crypto_shash_digest(desc, sb, mac - sb, mac);
-		if (unlikely(r < 0)) {
-			dm_integrity_io_error(ic, "crypto_shash_digest", r);
+		ahash_request_set_crypt(req, &sg, mac, nbytes);
+		r = crypto_wait_req(crypto_ahash_digest(req), &wait);
+		if (unlikely(r)) {
+			dm_integrity_io_error(ic, "crypto_ahash_digest", r);
+			ahash_request_free(req);
 			return r;
 		}
 	} else {
 		__u8 actual_mac[HASH_MAX_DIGESTSIZE];
 
-		r = crypto_shash_digest(desc, sb, mac - sb, actual_mac);
-		if (unlikely(r < 0)) {
-			dm_integrity_io_error(ic, "crypto_shash_digest", r);
+		ahash_request_set_crypt(req, &sg, actual_mac, nbytes);
+		r = crypto_wait_req(crypto_ahash_digest(req), &wait);
+		if (unlikely(r)) {
+			dm_integrity_io_error(ic, "crypto_ahash_digest", r);
+			ahash_request_free(req);
 			return r;
 		}
 		if (memcmp(mac, actual_mac, mac_size)) {
 			dm_integrity_io_error(ic, "superblock mac", -EILSEQ);
 			dm_audit_log_target(DM_MSG_PREFIX, "mac-superblock", ic->ti, 0);
+			ahash_request_free(req);
 			return -EILSEQ;
 		}
 	}
 
+	ahash_request_free(req);
 	return 0;
 }
 
@@ -775,51 +793,62 @@ static struct journal_sector *access_journal_data(struct dm_integrity_c *ic, uns
 
 static void section_mac(struct dm_integrity_c *ic, unsigned int section, __u8 result[JOURNAL_MAC_SIZE])
 {
-	SHASH_DESC_ON_STACK(desc, ic->journal_mac);
+	unsigned int j, size, nsg, nbytes = 0;
+	struct scatterlist *sg = NULL, *s;
+	struct ahash_request *req = NULL;
+	DECLARE_CRYPTO_WAIT(wait);
+	__le64 *section_le = NULL;
 	int r;
-	unsigned int j, size;
 
-	desc->tfm = ic->journal_mac;
+	req = ahash_request_alloc(ic->journal_mac, GFP_KERNEL);
+	if (unlikely(!req)) {
+		dm_integrity_io_error(ic, "ahash_request_alloc", -ENOMEM);
+		goto err;
+	}
+	ahash_request_set_callback(req, 0, crypto_req_done, &wait);
 
-	r = crypto_shash_init(desc);
-	if (unlikely(r < 0)) {
-		dm_integrity_io_error(ic, "crypto_shash_init", r);
+	nsg = ic->journal_section_entries;
+	if (ic->sb->flags & cpu_to_le32(SB_FLAG_FIXED_HMAC))
+		nsg += 2;
+	sg = kmalloc_array(nsg, sizeof(*sg), GFP_KERNEL);
+	if (unlikely(!sg)) {
+		dm_integrity_io_error(ic, "kmalloc_array", -ENOMEM);
 		goto err;
 	}
+	sg_init_table(sg, nsg);
+	s = sg;
 
 	if (ic->sb->flags & cpu_to_le32(SB_FLAG_FIXED_HMAC)) {
-		__le64 section_le;
-
-		r = crypto_shash_update(desc, (__u8 *)&ic->sb->salt, SALT_SIZE);
-		if (unlikely(r < 0)) {
-			dm_integrity_io_error(ic, "crypto_shash_update", r);
-			goto err;
-		}
+		sg_set_buf(s, (__u8 *)&ic->sb->salt, SALT_SIZE);
+		nbytes += SALT_SIZE;
+		s++;
 
-		section_le = cpu_to_le64(section);
-		r = crypto_shash_update(desc, (__u8 *)&section_le, sizeof(section_le));
-		if (unlikely(r < 0)) {
-			dm_integrity_io_error(ic, "crypto_shash_update", r);
+		section_le = kmalloc(sizeof(__le64), GFP_KERNEL);
+		if (unlikely(!section_le)) {
+			dm_integrity_io_error(ic, "kmalloc(sizeof(__le64))", -ENOMEM);
 			goto err;
 		}
+		*section_le = cpu_to_le64(section);
+		sg_set_buf(s, (__u8 *)section_le, sizeof(*section_le));
+		nbytes += sizeof(*section_le);
+		s++;
 	}
 
 	for (j = 0; j < ic->journal_section_entries; j++) {
 		struct journal_entry *je = access_journal_entry(ic, section, j);
 
-		r = crypto_shash_update(desc, (__u8 *)&je->u.sector, sizeof(je->u.sector));
-		if (unlikely(r < 0)) {
-			dm_integrity_io_error(ic, "crypto_shash_update", r);
-			goto err;
-		}
+		sg_set_buf(s, (__u8 *)&je->u.sector, sizeof(je->u.sector));
+		nbytes += sizeof(je->u.sector);
+		s++;
 	}
 
-	size = crypto_shash_digestsize(ic->journal_mac);
+	size = crypto_ahash_digestsize(ic->journal_mac);
 
 	if (likely(size <= JOURNAL_MAC_SIZE)) {
-		r = crypto_shash_final(desc, result);
-		if (unlikely(r < 0)) {
-			dm_integrity_io_error(ic, "crypto_shash_final", r);
+		ahash_request_set_crypt(req, sg, result, nbytes);
+		r = crypto_wait_req(crypto_ahash_digest(req), &wait);
+		if (unlikely(r)) {
+			dm_integrity_io_error(ic, "crypto_ahash_digest", r);
 			goto err;
 		}
 		memset(result + size, 0, JOURNAL_MAC_SIZE - size);
@@ -830,16 +859,24 @@ static void section_mac(struct dm_integrity_c *ic, unsigned int section, __u8 re
 			dm_integrity_io_error(ic, "digest_size", -EINVAL);
 			goto err;
 		}
-		r = crypto_shash_final(desc, digest);
-		if (unlikely(r < 0)) {
-			dm_integrity_io_error(ic, "crypto_shash_final", r);
+		ahash_request_set_crypt(req, sg, digest, nbytes);
+		r = crypto_wait_req(crypto_ahash_digest(req), &wait);
+		if (unlikely(r)) {
+			dm_integrity_io_error(ic, "crypto_ahash_digest", r);
 			goto err;
 		}
 		memcpy(result, digest, JOURNAL_MAC_SIZE);
 	}
 
+	ahash_request_free(req);
+	kfree(section_le);
+	kfree(sg);
 	return;
+
 err:
+	ahash_request_free(req);
+	kfree(section_le);
+	kfree(sg);
 	memset(result, 0, JOURNAL_MAC_SIZE);
 }
 
@@ -1637,53 +1674,65 @@ static void integrity_end_io(struct bio *bio)
 static void integrity_sector_checksum(struct dm_integrity_c *ic, sector_t sector,
 				      const char *data, char *result)
 {
-	__le64 sector_le = cpu_to_le64(sector);
-	SHASH_DESC_ON_STACK(req, ic->internal_hash);
-	int r;
+	struct ahash_request *req = NULL;
+	struct scatterlist sg[3], *s;
+	DECLARE_CRYPTO_WAIT(wait);
+	__le64 *sector_le = NULL;
 	unsigned int digest_size;
+	unsigned int nbytes = 0;
+	int r;
 
-	req->tfm = ic->internal_hash;
-
-	r = crypto_shash_init(req);
-	if (unlikely(r < 0)) {
-		dm_integrity_io_error(ic, "crypto_shash_init", r);
+	req = ahash_request_alloc(ic->internal_hash, GFP_KERNEL);
+	if (unlikely(!req)) {
+		dm_integrity_io_error(ic, "ahash_request_alloc", -ENOMEM);
 		goto failed;
 	}
+	ahash_request_set_callback(req, 0, crypto_req_done, &wait);
 
+	s = sg;
 	if (ic->sb->flags & cpu_to_le32(SB_FLAG_FIXED_HMAC)) {
-		r = crypto_shash_update(req, (__u8 *)&ic->sb->salt, SALT_SIZE);
-		if (unlikely(r < 0)) {
-			dm_integrity_io_error(ic, "crypto_shash_update", r);
-			goto failed;
-		}
+		sg_init_table(sg, 3);
+		sg_set_buf(s, (const __u8 *)&ic->sb->salt, SALT_SIZE);
+		nbytes += SALT_SIZE;
+		s++;
+	} else {
+		sg_init_table(sg, 2);
 	}
 
-	r = crypto_shash_update(req, (const __u8 *)&sector_le, sizeof(sector_le));
-	if (unlikely(r < 0)) {
-		dm_integrity_io_error(ic, "crypto_shash_update", r);
+	sector_le = kmalloc(sizeof(__le64), GFP_KERNEL);
+	if (unlikely(!sector_le)) {
+		dm_integrity_io_error(ic, "kmalloc(sizeof(__le64))", -ENOMEM);
 		goto failed;
 	}
+	*sector_le = cpu_to_le64(sector);
+	sg_set_buf(s, (const __u8 *)sector_le, sizeof(*sector_le));
+	nbytes += sizeof(*sector_le);
+	s++;
 
-	r = crypto_shash_update(req, data, ic->sectors_per_block << SECTOR_SHIFT);
-	if (unlikely(r < 0)) {
-		dm_integrity_io_error(ic, "crypto_shash_update", r);
-		goto failed;
-	}
+	sg_set_buf(s, data, ic->sectors_per_block << SECTOR_SHIFT);
+	nbytes += ic->sectors_per_block << SECTOR_SHIFT;
+
+	ahash_request_set_crypt(req, sg, result, nbytes);
 
-	r = crypto_shash_final(req, result);
-	if (unlikely(r < 0)) {
-		dm_integrity_io_error(ic, "crypto_shash_final", r);
+	r = crypto_wait_req(crypto_ahash_digest(req), &wait);
+	if (r) {
+		dm_integrity_io_error(ic, "crypto_ahash_digest", r);
 		goto failed;
 	}
 
-	digest_size = crypto_shash_digestsize(ic->internal_hash);
+	digest_size = crypto_ahash_digestsize(ic->internal_hash);
 	if (unlikely(digest_size < ic->tag_size))
 		memset(result + digest_size, 0, ic->tag_size - digest_size);
 
+	ahash_request_free(req);
+	kfree(sector_le);
+
 	return;
 
 failed:
 	/* this shouldn't happen anyway, the hash functions have no reason to fail */
+	ahash_request_free(req);
+	kfree(sector_le);
 	get_random_bytes(result, ic->tag_size);
 }
 
@@ -1776,7 +1825,7 @@ static void integrity_metadata(struct work_struct *w)
 	if (ic->internal_hash) {
 		struct bvec_iter iter;
 		struct bio_vec bv;
-		unsigned int digest_size = crypto_shash_digestsize(ic->internal_hash);
+		unsigned int digest_size = crypto_ahash_digestsize(ic->internal_hash);
 		struct bio *bio = dm_bio_from_per_bio_data(dio, sizeof(struct dm_integrity_io));
 		char *checksums;
 		unsigned int extra_space = unlikely(digest_size > ic->tag_size) ? digest_size - ic->tag_size : 0;
@@ -2124,7 +2173,7 @@ static bool __journal_read_write(struct dm_integrity_io *dio, struct bio *bio,
 				} while (++s < ic->sectors_per_block);
 
 				if (ic->internal_hash) {
-					unsigned int digest_size = crypto_shash_digestsize(ic->internal_hash);
+					unsigned int digest_size = crypto_ahash_digestsize(ic->internal_hash);
 
 					if (unlikely(digest_size > ic->tag_size)) {
 						char checksums_onstack[HASH_MAX_DIGESTSIZE];
@@ -2428,7 +2477,7 @@ static int dm_integrity_map_inline(struct dm_integrity_io *dio, bool from_map)
 	if (!dio->integrity_payload) {
 		unsigned digest_size, extra_size;
 		dio->payload_len = ic->tuple_size * (bio_sectors(bio) >> ic->sb->log2_sectors_per_block);
-		digest_size = crypto_shash_digestsize(ic->internal_hash);
+		digest_size = crypto_ahash_digestsize(ic->internal_hash);
 		extra_size = unlikely(digest_size > ic->tag_size) ? digest_size - ic->tag_size : 0;
 		dio->payload_len += extra_size;
 		dio->integrity_payload = kmalloc(dio->payload_len, GFP_NOIO | __GFP_NORETRY | __GFP_NOMEMALLOC | __GFP_NOWARN);
@@ -2595,7 +2644,7 @@ static void dm_integrity_inline_recheck(struct work_struct *w)
 		bio_put(outgoing_bio);
 
 		integrity_sector_checksum(ic, dio->bio_details.bi_iter.bi_sector, outgoing_data, digest);
-		if (unlikely(memcmp(digest, dio->integrity_payload, min(crypto_shash_digestsize(ic->internal_hash), ic->tag_size)))) {
+		if (unlikely(memcmp(digest, dio->integrity_payload, min(crypto_ahash_digestsize(ic->internal_hash), ic->tag_size)))) {
 			DMERR_LIMIT("%pg: Checksum failed at sector 0x%llx",
 				ic->dev->bdev, dio->bio_details.bi_iter.bi_sector);
 			atomic64_inc(&ic->number_of_mismatches);
@@ -2635,7 +2684,7 @@ static int dm_integrity_end_io(struct dm_target *ti, struct bio *bio, blk_status
 				//memset(mem, 0xff, ic->sectors_per_block << SECTOR_SHIFT);
 				integrity_sector_checksum(ic, dio->bio_details.bi_iter.bi_sector, mem, digest);
 				if (unlikely(memcmp(digest, dio->integrity_payload + pos,
-						min(crypto_shash_digestsize(ic->internal_hash), ic->tag_size)))) {
+						min(crypto_ahash_digestsize(ic->internal_hash), ic->tag_size)))) {
 					kunmap_local(mem);
 					dm_integrity_free_payload(dio);
 					INIT_WORK(&dio->work, dm_integrity_inline_recheck);
@@ -3017,8 +3066,8 @@ static void integrity_recalc(struct work_struct *w)
 		goto free_ret;
 	}
 	recalc_tags_size = (recalc_sectors >> ic->sb->log2_sectors_per_block) * ic->tag_size;
-	if (crypto_shash_digestsize(ic->internal_hash) > ic->tag_size)
-		recalc_tags_size += crypto_shash_digestsize(ic->internal_hash) - ic->tag_size;
+	if (crypto_ahash_digestsize(ic->internal_hash) > ic->tag_size)
+		recalc_tags_size += crypto_ahash_digestsize(ic->internal_hash) - ic->tag_size;
 	recalc_tags = kvmalloc(recalc_tags_size, GFP_NOIO);
 	if (!recalc_tags) {
 		vfree(recalc_buffer);
@@ -3177,8 +3226,8 @@ static void integrity_recalc_inline(struct work_struct *w)
 	}
 
 	recalc_tags_size = (recalc_sectors >> ic->sb->log2_sectors_per_block) * ic->tuple_size;
-	if (crypto_shash_digestsize(ic->internal_hash) > ic->tuple_size)
-		recalc_tags_size += crypto_shash_digestsize(ic->internal_hash) - ic->tuple_size;
+	if (crypto_ahash_digestsize(ic->internal_hash) > ic->tuple_size)
+		recalc_tags_size += crypto_ahash_digestsize(ic->internal_hash) - ic->tuple_size;
 	recalc_tags = kmalloc(recalc_tags_size, GFP_NOIO | __GFP_NOWARN);
 	if (!recalc_tags) {
 		kfree(recalc_buffer);
@@ -4187,6 +4236,8 @@ static int get_alg_and_key(const char *arg, struct alg_spec *a, char **error, ch
 	if (!a->alg_string)
 		goto nomem;
 
+	DEBUG_print("%s: alg_string=%s\n", __func__, a->alg_string);
+
 	k = strchr(a->alg_string, ':');
 	if (k) {
 		*k = 0;
@@ -4198,6 +4249,9 @@ static int get_alg_and_key(const char *arg, struct alg_spec *a, char **error, ch
 		a->key = kmalloc(a->key_size, GFP_KERNEL);
 		if (!a->key)
 			goto nomem;
+
+		DEBUG_print("%s: key=%s\n", __func__, a->key_string);
+
 		if (hex2bin(a->key, a->key_string, a->key_size))
 			goto inval;
 	}
@@ -4211,13 +4265,15 @@ static int get_alg_and_key(const char *arg, struct alg_spec *a, char **error, ch
 	return -ENOMEM;
 }
 
-static int get_mac(struct crypto_shash **hash, struct alg_spec *a, char **error,
+static int get_mac(struct crypto_ahash **hash, struct alg_spec *a, char **error,
 		   char *error_alg, char *error_key)
 {
 	int r;
 
+	DEBUG_print(">%s\n", __func__);
+
 	if (a->alg_string) {
-		*hash = crypto_alloc_shash(a->alg_string, 0, CRYPTO_ALG_ALLOCATES_MEMORY);
+		*hash = crypto_alloc_ahash(a->alg_string, 0, CRYPTO_ALG_ALLOCATES_MEMORY);
 		if (IS_ERR(*hash)) {
 			*error = error_alg;
 			r = PTR_ERR(*hash);
@@ -4226,12 +4282,12 @@ static int get_mac(struct crypto_shash **hash, struct alg_spec *a, char **error,
 		}
 
 		if (a->key) {
-			r = crypto_shash_setkey(*hash, a->key, a->key_size);
+			r = crypto_ahash_setkey(*hash, a->key, a->key_size);
 			if (r) {
 				*error = error_key;
 				return r;
 			}
-		} else if (crypto_shash_get_flags(*hash) & CRYPTO_TFM_NEED_KEY) {
+		} else if (crypto_ahash_get_flags(*hash) & CRYPTO_TFM_NEED_KEY) {
 			*error = error_key;
 			return -ENOKEY;
 		}
@@ -4707,7 +4763,7 @@ static int dm_integrity_ctr(struct dm_target *ti, unsigned int argc, char **argv
 			r = -EINVAL;
 			goto bad;
 		}
-		ic->tag_size = crypto_shash_digestsize(ic->internal_hash);
+		ic->tag_size = crypto_ahash_digestsize(ic->internal_hash);
 	}
 	if (ic->tag_size > MAX_TAG_SIZE) {
 		ti->error = "Too big tag size";
@@ -5226,7 +5282,7 @@ static void dm_integrity_dtr(struct dm_target *ti)
 		free_pages_exact(ic->sb, SB_SECTORS << SECTOR_SHIFT);
 
 	if (ic->internal_hash)
-		crypto_free_shash(ic->internal_hash);
+		crypto_free_ahash(ic->internal_hash);
 	free_alg(&ic->internal_hash_alg);
 
 	if (ic->journal_crypt)
@@ -5234,7 +5290,7 @@ static void dm_integrity_dtr(struct dm_target *ti)
 	free_alg(&ic->journal_crypt_alg);
 
 	if (ic->journal_mac)
-		crypto_free_shash(ic->journal_mac);
+		crypto_free_ahash(ic->journal_mac);
 	free_alg(&ic->journal_mac_alg);
 
 	kfree(ic);
-- 
2.43.0


  reply	other threads:[~2025-01-15 16:47 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-01-15 16:46 [PATCH v1 0/1] dm-integrity: Implement asynch digest support Harald Freudenberger
2025-01-15 16:46 ` Harald Freudenberger [this message]
2025-01-15 17:29   ` [PATCH v1 1/1] " Mikulas Patocka
2025-01-17 13:31     ` Harald Freudenberger
2025-01-22 17:00     ` Harald Freudenberger
2025-01-27 17:57       ` Mikulas Patocka
2025-01-15 17:37   ` Eric Biggers
2025-01-16  7:33     ` Harald Freudenberger
2025-01-16  8:03       ` Eric Biggers
2025-01-16  9:00         ` Harald Freudenberger
2025-01-16  9:12           ` Herbert Xu
2025-01-16 17:54             ` Eric Biggers
2025-01-17  6:21               ` Herbert Xu
2025-01-17  7:57                 ` Eric Biggers

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=20250115164657.84650-2-freude@linux.ibm.com \
    --to=freude@linux.ibm.com \
    --cc=agk@redhat.com \
    --cc=dengler@linux.ibm.com \
    --cc=dm-devel@lists.linux.dev \
    --cc=herbert@gondor.apana.org.au \
    --cc=ifranzki@linux.ibm.com \
    --cc=linux-s390@vger.kernel.org \
    --cc=mpatocka@redhat.com \
    --cc=snitzer@kernel.org \
    /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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.