From: Cong Wang <xiyou.wangcong@gmail.com>
To: Andy Lutomirski <luto@amacapital.net>
Cc: Kees Cook <kees@kernel.org>,
linux-kernel@vger.kernel.org, Will Drewry <wad@chromium.org>,
Christian Brauner <brauner@kernel.org>,
Andrew Morton <akpm@linux-foundation.org>,
linux-mm@kvack.org, Cong Wang <cwang@multikernel.io>
Subject: [PATCH v5 4/7] seccomp: add kernel-installed pinned-memfd redirect
Date: Sat, 4 Jul 2026 16:18:28 -0700 [thread overview]
Message-ID: <20260704231831.354543-5-xiyou.wangcong@gmail.com> (raw)
In-Reply-To: <20260704231831.354543-1-xiyou.wangcong@gmail.com>
From: Cong Wang <cwang@multikernel.io>
Add SECCOMP_IOCTL_NOTIF_SEND_REDIRECT, which resumes a trapped syscall
(like SECCOMP_USER_NOTIF_FLAG_CONTINUE) with selected argument registers
rewritten to point into a pin installed by SECCOMP_IOCTL_NOTIF_PIN_INSTALL.
This closes the user-notification TOCTOU for fork+execve sandboxes: the
kernel acts on an immutable, supervisor-controlled sealed mapping instead
of memory a CLONE_VM peer can rewrite after the check.
The feature is gated behind SECCOMP_FILTER_FLAG_REDIRECT, declared at
listener creation (it requires SECCOMP_FILTER_FLAG_NEW_LISTENER) and
required by both ioctls. At most one redirect-capable filter may exist in
a chain (-EBUSY otherwise), so a redirect has a single, unambiguous
register fixup.
The supervisor supplies an args_mask, a ptr_mask and replacement values.
Each pointer substitution is validated by seccomp_pin_check(): the access
[args[i], args[i] + ptr_len[i]) must lie in a single VM_SEALED, read-only,
MAP_SHARED VMA still backed by the named memfd. The kernel keeps no
bookkeeping; after execve or exit the VMA is gone and validation returns
-EFAULT.
Original arg registers are saved and restored at user-mode return (via a
TWA_RESUME task_work, so a restartable syscall is not turned into a
livelock), preserving the caller-saved arg-register ABI. The restore is
skipped after a successful execve. rt_sigreturn is refused (-EOPNOTSUPP):
it restores the whole register frame and takes no arguments to substitute.
Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
include/linux/seccomp.h | 7 +-
include/uapi/linux/seccomp.h | 55 ++++++-
kernel/seccomp.c | 290 +++++++++++++++++++++++++++++++++++
3 files changed, 350 insertions(+), 2 deletions(-)
diff --git a/include/linux/seccomp.h b/include/linux/seccomp.h
index a91d1fc8a2b8..5d53f8fce508 100644
--- a/include/linux/seccomp.h
+++ b/include/linux/seccomp.h
@@ -10,7 +10,8 @@
SECCOMP_FILTER_FLAG_SPEC_ALLOW | \
SECCOMP_FILTER_FLAG_NEW_LISTENER | \
SECCOMP_FILTER_FLAG_TSYNC_ESRCH | \
- SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV)
+ SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV | \
+ SECCOMP_FILTER_FLAG_REDIRECT)
/* sizeof() the first published struct seccomp_notif_addfd */
#define SECCOMP_NOTIFY_ADDFD_SIZE_VER0 24
@@ -21,6 +22,10 @@
#define SECCOMP_NOTIFY_PIN_INSTALL_SIZE_VER1 40 /* adds @offset */
#define SECCOMP_NOTIFY_PIN_INSTALL_SIZE_LATEST SECCOMP_NOTIFY_PIN_INSTALL_SIZE_VER1
+/* sizeof() the first published struct seccomp_notif_resp_redirect */
+#define SECCOMP_NOTIFY_RESP_REDIRECT_SIZE_VER0 120
+#define SECCOMP_NOTIFY_RESP_REDIRECT_SIZE_LATEST SECCOMP_NOTIFY_RESP_REDIRECT_SIZE_VER0
+
#ifdef CONFIG_SECCOMP
#include <linux/thread_info.h>
diff --git a/include/uapi/linux/seccomp.h b/include/uapi/linux/seccomp.h
index d3249294788b..bb5875f72556 100644
--- a/include/uapi/linux/seccomp.h
+++ b/include/uapi/linux/seccomp.h
@@ -25,6 +25,12 @@
#define SECCOMP_FILTER_FLAG_TSYNC_ESRCH (1UL << 4)
/* Received notifications wait in killable state (only respond to fatal signals) */
#define SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV (1UL << 5)
+/*
+ * Declares that this listener's notifier may issue
+ * SECCOMP_IOCTL_NOTIF_PIN_INSTALL / SECCOMP_IOCTL_NOTIF_SEND_REDIRECT. At most
+ * one such filter may exist in a task's filter chain. Requires NEW_LISTENER.
+ */
+#define SECCOMP_FILTER_FLAG_REDIRECT (1UL << 6)
/*
* All BPF programs must return a 32-bit value.
@@ -139,7 +145,9 @@ struct seccomp_notif_addfd {
/**
* struct seccomp_notif_pin_install - have the kernel install a sealed
- * MAP_SHARED mapping of @memfd into the trapped task's mm at @target_addr.
+ * MAP_SHARED mapping of @memfd into the trapped task's mm at @target_addr,
+ * which SECCOMP_IOCTL_NOTIF_SEND_REDIRECT can then use as a target for
+ * substituted pointer arguments.
*
* The supervisor owns @memfd and the kernel installs the mapping without
* target-side cooperation. It is read-only and VM_SEALED, so the target and
@@ -168,6 +176,45 @@ struct seccomp_notif_pin_install {
__u64 offset;
};
+#define SECCOMP_REDIRECT_ARGS 6
+
+/**
+ * struct seccomp_notif_resp_redirect - resume the trapped syscall with
+ * substituted arg-register values, optionally pointing into an installed
+ * pinned-memfd region.
+ *
+ * Like SECCOMP_USER_NOTIF_FLAG_CONTINUE the syscall runs, but the kernel
+ * first rewrites the arg registers in @args_mask. Pointer substitutions
+ * (@ptr_mask) are validated against the trapped task's live mapping of
+ * @memfd, so a target that has exited or execve()d simply fails validation.
+ * Original registers are restored at syscall exit, skipped after a successful
+ * execve whose fresh register file must not be clobbered.
+ *
+ * @id: The ID of the seccomp notification this response consumes.
+ * @flags: SECCOMP_REDIRECT_FLAG_*. CONTINUE must be set.
+ * @args_mask: Bit i set means args[i] replaces arg register i before the
+ * syscall runs.
+ * @ptr_mask: Subset of @args_mask. Bit i set means args[i] is a pointer whose
+ * access [args[i], args[i] + ptr_len[i]) must lie inside a single
+ * VM_SEALED, read-only mapping of @memfd. Scalars (in @args_mask
+ * but not @ptr_mask) are written verbatim.
+ * @memfd: Supervisor-side fd for the backing memfd. Consulted only when
+ * @ptr_mask is non-zero.
+ * @args: Replacement values for the arg registers.
+ * @ptr_len: For each bit set in @ptr_mask, the byte length of the access at
+ * args[i]; must be non-zero and args[i] + ptr_len[i] must not
+ * overflow. Must be 0 where @ptr_mask bit i is clear.
+ */
+struct seccomp_notif_resp_redirect {
+ __u64 id;
+ __u32 flags;
+ __u32 args_mask;
+ __u32 ptr_mask;
+ __u32 memfd;
+ __u64 args[SECCOMP_REDIRECT_ARGS];
+ __u64 ptr_len[SECCOMP_REDIRECT_ARGS];
+};
+
#define SECCOMP_IOC_MAGIC '!'
#define SECCOMP_IO(nr) _IO(SECCOMP_IOC_MAGIC, nr)
#define SECCOMP_IOR(nr, type) _IOR(SECCOMP_IOC_MAGIC, nr, type)
@@ -188,4 +235,10 @@ struct seccomp_notif_pin_install {
#define SECCOMP_IOCTL_NOTIF_PIN_INSTALL SECCOMP_IOWR(5, \
struct seccomp_notif_pin_install)
+#define SECCOMP_IOCTL_NOTIF_SEND_REDIRECT SECCOMP_IOW(6, \
+ struct seccomp_notif_resp_redirect)
+
+/* Valid flags for struct seccomp_notif_resp_redirect. */
+#define SECCOMP_REDIRECT_FLAG_CONTINUE (1UL << 0)
+
#endif /* _UAPI_LINUX_SECCOMP_H */
diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 1c0b3bb71379..c5a01ae097d1 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -233,6 +233,7 @@ struct seccomp_filter {
refcount_t users;
bool log;
bool wait_killable_recv;
+ bool redirect_capable;
struct action_cache cache;
struct seccomp_filter *prev;
struct bpf_prog *prog;
@@ -953,6 +954,13 @@ static long seccomp_attach_filter(unsigned int flags,
}
}
+ if (flags & SECCOMP_FILTER_FLAG_REDIRECT) {
+ for (walker = current->seccomp.filter; walker;
+ walker = walker->prev)
+ if (walker->redirect_capable)
+ return -EBUSY;
+ }
+
/* Set log flag, if present. */
if (flags & SECCOMP_FILTER_FLAG_LOG)
filter->log = true;
@@ -961,6 +969,10 @@ static long seccomp_attach_filter(unsigned int flags,
if (flags & SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV)
filter->wait_killable_recv = true;
+ /* Set redirect-capable flag, if present. */
+ if (flags & SECCOMP_FILTER_FLAG_REDIRECT)
+ filter->redirect_capable = true;
+
/*
* If there is an existing filter, make it the prev and don't drop its
* task reference.
@@ -1941,6 +1953,277 @@ static long seccomp_notify_pin_install(struct seccomp_filter *filter,
return ret;
}
+static bool seccomp_pin_check(struct task_struct *target,
+ struct file *memfd_file, u64 ptr, u64 len)
+{
+ struct vm_area_struct *vma;
+ struct mm_struct *mm;
+ bool ok = false;
+ u64 end;
+
+ if (!len)
+ return false;
+ end = ptr + len;
+ if (end < ptr)
+ return false;
+
+ mm = get_task_mm(target);
+ if (!mm)
+ return false;
+
+ mmap_read_lock(mm);
+ vma = vma_lookup(mm, ptr);
+ /*
+ * The access must lie in a single sealed, read-only, MAP_SHARED,
+ * memfd-backed VMA. VM_SHARED is required so the bytes the kernel reads
+ * are the memfd's own pages: a MAP_PRIVATE mapping would resolve to
+ * anonymous COW copies the target could have written before sealing it
+ * read-only, defeating the guarantee.
+ */
+ if (vma && end <= vma->vm_end && (vma->vm_flags & VM_SEALED) &&
+ (vma->vm_flags & VM_SHARED) && !(vma->vm_flags & VM_WRITE) &&
+ vma->vm_file && file_inode(vma->vm_file) == file_inode(memfd_file))
+ ok = true;
+ mmap_read_unlock(mm);
+
+ mmput(mm);
+ return ok;
+}
+
+struct seccomp_redirect_restore {
+ struct callback_head twork;
+ unsigned long orig_args[SECCOMP_REDIRECT_ARGS];
+ u32 args_mask; /* bit i: arg i was substituted, restore it */
+ u64 self_exec_id; /* snapshot to detect an intervening execve */
+};
+
+/*
+ * If a syscall is redirected by more than one filter, one restore is
+ * queued per redirect, each recording the args as they stood at its own
+ * redirect. Correct restoration relies on task_work running LIFO:
+ * the first redirect captured the true original and queued first, runs
+ * last, so it wins.
+ */
+static void seccomp_redirect_restore_cb(struct callback_head *cb)
+{
+ struct seccomp_redirect_restore *r =
+ container_of(cb, struct seccomp_redirect_restore, twork);
+ unsigned long args[SECCOMP_REDIRECT_ARGS];
+ long ret, err;
+ int i;
+
+ if (READ_ONCE(current->self_exec_id) != r->self_exec_id) {
+ kfree(r);
+ return;
+ }
+
+ err = syscall_get_error(current, current_pt_regs());
+ ret = syscall_get_return_value(current, current_pt_regs());
+
+ syscall_get_arguments(current, current_pt_regs(), args);
+ for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++)
+ if (r->args_mask & (1U << i))
+ args[i] = r->orig_args[i];
+ syscall_set_arguments(current, current_pt_regs(), args);
+
+ syscall_set_return_value(current, current_pt_regs(), err, ret);
+
+ kfree(r);
+}
+
+/*
+ * sigreturn/rt_sigreturn restore the entire register frame from the user
+ * signal stack; the SEND_REDIRECT register-restore (run from task_work at
+ * user-mode return) would corrupt that frame, and the syscall takes no
+ * arguments to substitute anyway. Refuse to redirect any of them, including
+ * the compat variants (legacy sigreturn and rt_sigreturn are distinct
+ * numbers). x32 rt_sigreturn carries __X32_SYSCALL_BIT and is not matched
+ * here; the deprecated x32 ABI is out of scope for redirect.
+ */
+static bool seccomp_redirect_is_sigreturn(const struct seccomp_data *sd)
+{
+#ifdef SECCOMP_ARCH_COMPAT
+ if (sd->arch == SECCOMP_ARCH_COMPAT)
+ return sd->nr == __NR_seccomp_sigreturn_32 ||
+ sd->nr == __NR_seccomp_rt_sigreturn_32;
+#endif
+ return sd->nr == __NR_seccomp_sigreturn ||
+ sd->nr == __NR_seccomp_rt_sigreturn;
+}
+
+/*
+ * clone/fork-family syscalls copy the trapped task's register frame into the
+ * new child, which gets no restore task_work of its own. A redirected task
+ * creation would leave the child in user space with the substituted (pinned)
+ * values still in its caller-saved arg registers, breaking the ABI the restore
+ * preserves for the parent. There is no use case for redirecting task creation,
+ * so refuse it. Syscalls absent on an arch resolve to -1 and never match.
+ */
+static bool seccomp_redirect_is_task_create(const struct seccomp_data *sd)
+{
+#ifdef SECCOMP_ARCH_COMPAT
+ if (sd->arch == SECCOMP_ARCH_COMPAT)
+ return sd->nr == __NR_seccomp_clone_32 ||
+ sd->nr == __NR_seccomp_clone3_32 ||
+ sd->nr == __NR_seccomp_fork_32 ||
+ sd->nr == __NR_seccomp_vfork_32;
+#endif
+ return sd->nr == __NR_seccomp_clone ||
+ sd->nr == __NR_seccomp_clone3 ||
+ sd->nr == __NR_seccomp_fork ||
+ sd->nr == __NR_seccomp_vfork;
+}
+
+static long seccomp_notify_send_redirect(struct seccomp_filter *filter,
+ struct seccomp_notif_resp_redirect __user *uresp,
+ unsigned int size)
+{
+ unsigned long args[SECCOMP_REDIRECT_ARGS];
+ struct seccomp_redirect_restore *restore;
+ struct seccomp_notif_resp_redirect resp;
+ struct file *memfd_file = NULL;
+ struct seccomp_knotif *knotif;
+ struct pt_regs *target_regs;
+ long ret;
+ int i;
+
+ BUILD_BUG_ON(sizeof(resp) < SECCOMP_NOTIFY_RESP_REDIRECT_SIZE_VER0);
+ BUILD_BUG_ON(sizeof(resp) != SECCOMP_NOTIFY_RESP_REDIRECT_SIZE_LATEST);
+
+ if (!filter->redirect_capable)
+ return -EPERM;
+
+ if (size < SECCOMP_NOTIFY_RESP_REDIRECT_SIZE_VER0 || size >= PAGE_SIZE)
+ return -EINVAL;
+
+ ret = copy_struct_from_user(&resp, sizeof(resp), uresp, size);
+ if (ret)
+ return ret;
+
+ if (!(resp.flags & SECCOMP_REDIRECT_FLAG_CONTINUE))
+ return -EINVAL;
+ if (resp.flags & ~SECCOMP_REDIRECT_FLAG_CONTINUE)
+ return -EINVAL;
+ if (resp.args_mask & ~((1U << SECCOMP_REDIRECT_ARGS) - 1))
+ return -EINVAL;
+ if (resp.ptr_mask & ~resp.args_mask)
+ return -EINVAL;
+ if (!resp.args_mask)
+ return -EINVAL;
+ for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++) {
+ if (resp.ptr_mask & (1U << i)) {
+ if (!resp.ptr_len[i])
+ return -EINVAL;
+ } else if (resp.ptr_len[i]) {
+ return -EINVAL;
+ }
+ }
+ if (resp.ptr_mask) {
+ memfd_file = fget(resp.memfd);
+ if (!memfd_file)
+ return -EBADF;
+ }
+
+ restore = kzalloc_obj(*restore, GFP_KERNEL_ACCOUNT);
+ if (!restore) {
+ ret = -ENOMEM;
+ goto out_free;
+ }
+ init_task_work(&restore->twork, seccomp_redirect_restore_cb);
+
+ ret = mutex_lock_interruptible(&filter->notify_lock);
+ if (ret < 0)
+ goto out_free;
+
+ knotif = find_notification(filter, resp.id);
+ if (!knotif) {
+ ret = -ENOENT;
+ goto out_unlock_free;
+ }
+ if (knotif->state != SECCOMP_NOTIFY_SENT) {
+ ret = -EINPROGRESS;
+ goto out_unlock_free;
+ }
+
+ if (seccomp_redirect_is_sigreturn(knotif->data) ||
+ seccomp_redirect_is_task_create(knotif->data)) {
+ ret = -EOPNOTSUPP;
+ goto out_unlock_free;
+ }
+
+#ifdef SECCOMP_ARCH_COMPAT
+ if (knotif->data->arch == SECCOMP_ARCH_COMPAT) {
+ for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++)
+ if (resp.ptr_mask & (1U << i))
+ resp.args[i] = (u32)resp.args[i];
+ }
+#endif
+
+ for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++) {
+ if (!(resp.ptr_mask & (1U << i)))
+ continue;
+ if (!seccomp_pin_check(knotif->task, memfd_file,
+ resp.args[i], resp.ptr_len[i])) {
+ ret = -EFAULT;
+ goto out_unlock_free;
+ }
+ }
+
+ target_regs = task_pt_regs(knotif->task);
+ syscall_get_arguments(knotif->task, target_regs, args);
+
+ for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++)
+ restore->orig_args[i] = args[i];
+ restore->args_mask = resp.args_mask;
+ restore->self_exec_id = READ_ONCE(knotif->task->self_exec_id);
+
+ for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++)
+ if (resp.args_mask & (1U << i))
+ args[i] = resp.args[i];
+ syscall_set_arguments(knotif->task, target_regs, args);
+
+ /*
+ * Use TWA_RESUME, not TWA_SIGNAL. TWA_SIGNAL sets TIF_NOTIFY_SIGNAL,
+ * which makes signal_pending() true for the entire redirected syscall
+ * An interruptible syscall would then bail out with -ERESTARTSYS before
+ * doing any work, restart, re-trap and get redirected again, it is a
+ * livelock. TWA_RESUME does not feed signal_pending(), and the restore
+ * still runs before signal delivery: get_signal() runs task_work_run()
+ * before it dequeues a signal, so the original args are back in pt_regs
+ * before handle_signal() builds the sigframe or the -ERESTART* path
+ * rewinds for restart.
+ */
+ ret = task_work_add(knotif->task, &restore->twork, TWA_RESUME);
+ if (ret) {
+ for (i = 0; i < SECCOMP_REDIRECT_ARGS; i++)
+ args[i] = restore->orig_args[i];
+ syscall_set_arguments(knotif->task, target_regs, args);
+ goto out_unlock_free;
+ }
+
+ knotif->state = SECCOMP_NOTIFY_REPLIED;
+ knotif->error = 0;
+ knotif->val = 0;
+ knotif->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
+ if (filter->notif->flags & SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP)
+ complete_on_current_cpu(&knotif->ready);
+ else
+ complete(&knotif->ready);
+
+ mutex_unlock(&filter->notify_lock);
+ if (memfd_file)
+ fput(memfd_file);
+ return 0;
+
+out_unlock_free:
+ mutex_unlock(&filter->notify_lock);
+out_free:
+ if (memfd_file)
+ fput(memfd_file);
+ kfree(restore);
+ return ret;
+}
+
static long seccomp_notify_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
@@ -1968,6 +2251,9 @@ static long seccomp_notify_ioctl(struct file *file, unsigned int cmd,
case EA_IOCTL(SECCOMP_IOCTL_NOTIF_PIN_INSTALL):
return seccomp_notify_pin_install(filter, buf,
_IOC_SIZE(cmd));
+ case EA_IOCTL(SECCOMP_IOCTL_NOTIF_SEND_REDIRECT):
+ return seccomp_notify_send_redirect(filter, buf,
+ _IOC_SIZE(cmd));
default:
return -EINVAL;
}
@@ -2107,6 +2393,10 @@ static long seccomp_set_mode_filter(unsigned int flags,
((flags & SECCOMP_FILTER_FLAG_NEW_LISTENER) == 0))
return -EINVAL;
+ if ((flags & SECCOMP_FILTER_FLAG_REDIRECT) &&
+ ((flags & SECCOMP_FILTER_FLAG_NEW_LISTENER) == 0))
+ return -EINVAL;
+
/* Prepare the new filter before holding any locks. */
prepared = seccomp_prepare_user_filter(filter);
if (IS_ERR(prepared))
--
2.43.0
next prev parent reply other threads:[~2026-07-04 23:19 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-04 23:18 [PATCH v5 0/7] seccomp: non-cooperative pinned-memfd argument redirect Cong Wang
2026-07-04 23:18 ` [PATCH v5 1/7] mm: add __do_mmap() and vm_mmap_remote()/vm_munmap_remote() Cong Wang
2026-07-04 23:18 ` [PATCH v5 2/7] seccomp: introduce SECCOMP_IOCTL_NOTIF_PIN_INSTALL Cong Wang
2026-07-04 23:18 ` [PATCH v5 3/7] seccomp: add __NR_seccomp_* aliases for rt_sigreturn and clone/fork Cong Wang
2026-07-04 23:18 ` Cong Wang [this message]
2026-07-04 23:18 ` [PATCH v5 5/7] seccomp: re-validate a redirected syscall against outer filters Cong Wang
2026-07-04 23:18 ` [PATCH v5 6/7] docs/seccomp: document pinned-memfd redirect ioctls Cong Wang
2026-07-04 23:18 ` [PATCH v5 7/7] selftests/seccomp: cover non-cooperative pinned-memfd install Cong Wang
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=20260704231831.354543-5-xiyou.wangcong@gmail.com \
--to=xiyou.wangcong@gmail.com \
--cc=akpm@linux-foundation.org \
--cc=brauner@kernel.org \
--cc=cwang@multikernel.io \
--cc=kees@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=luto@amacapital.net \
--cc=wad@chromium.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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox