linux-kernel.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Reinette Chatre <reinette.chatre@intel.com>
To: dave.hansen@linux.intel.com, jarkko@kernel.org,
	tglx@linutronix.de, bp@alien8.de, luto@kernel.org,
	mingo@redhat.com, linux-sgx@vger.kernel.org, x86@kernel.org
Cc: seanjc@google.com, kai.huang@intel.com, cathy.zhang@intel.com,
	cedric.xing@intel.com, haitao.huang@intel.com,
	mark.shanahan@intel.com, hpa@zytor.com,
	linux-kernel@vger.kernel.org
Subject: [PATCH V3 14/30] x86/sgx: Support restricting of enclave page permissions
Date: Mon,  4 Apr 2022 09:49:22 -0700	[thread overview]
Message-ID: <8ed9ee98ca26c9eefde0fd49062bca6e7b9efe80.1648847675.git.reinette.chatre@intel.com> (raw)
In-Reply-To: <cover.1648847675.git.reinette.chatre@intel.com>

In the initial (SGX1) version of SGX, pages in an enclave need to be
created with permissions that support all usages of the pages, from the
time the enclave is initialized until it is unloaded. For example,
pages used by a JIT compiler or when code needs to otherwise be
relocated need to always have RWX permissions.

SGX2 includes a new function ENCLS[EMODPR] that is run from the kernel
and can be used to restrict the EPCM permissions of regular enclave
pages within an initialized enclave.

Introduce ioctl() SGX_IOC_ENCLAVE_RESTRICT_PERMISSIONS to support
restricting EPCM permissions. With this ioctl() the user specifies
a page range and the EPCM permissions to be applied to all pages in
the provided range. ENCLS[EMODPR] is run to restrict the EPCM
permissions followed by the ENCLS[ETRACK] flow that will ensure
no cached linear-to-physical address mappings to the changed
pages remain.

It is possible for the permission change request to fail on any
page within the provided range, either with an error encountered
by the kernel or by the SGX hardware while running
ENCLS[EMODPR]. To support partial success the ioctl() returns an
error code based on failures encountered by the kernel as well
as two result output parameters: one for the number of pages
that were successfully changed and one for the SGX return code.

The page table entry permissions are not impacted by the EPCM
permission changes. VMAs and PTEs will continue to allow the
maximum vetted permissions determined at the time the pages
are added to the enclave. The SGX error code in a page fault
will indicate if it was an EPCM permission check that prevented
an access attempt.

No checking is done to ensure that the permissions are actually
being restricted. This is because the enclave may have relaxed
the EPCM permissions from within the enclave without letting the
kernel know. An attempt to relax permissions using this call will
be ignored by the hardware.

Signed-off-by: Reinette Chatre <reinette.chatre@intel.com>
---
Changes since V2:
- Include the sgx_ioc_sgx2_ready() utility
  that previously was in "x86/sgx: Support relaxing of enclave page
  permissions" that is removed from the next version.
- Few renames requested by Jarkko:
  struct sgx_enclave_restrict_perm ->
         struct sgx_enclave_restrict_permissions
  sgx_enclave_restrict_perm()     ->
         sgx_enclave_restrict_permissions()
  sgx_ioc_enclave_restrict_perm() ->
         sgx_ioc_enclave_restrict_permissions()
- Make EPCM permissions independent from kernel view of
  permissions.  (Jarkko)
  - Remove attempt at runtime tracking of EPCM permissions
    (sgx_encl_page->vm_run_prot_bits).
  - Do not flush page table entries - they are no longer impacted by
    EPCM permission changes.
  - Modify changelog to reflect new architecture.
- Ensure at least PROT_READ is requested - enclave requires read
  access to the page for commands like EMODPE and EACCEPT. (Jarkko)

Changes since V1:
- Change terminology to use "relax" instead of "extend" to refer to
  the case when enclave page permissions are added (Dave).
- Use ioctl() in commit message (Dave).
- Add examples on what permissions would be allowed (Dave).
- Split enclave page permission changes into two ioctl()s, one for
  permission restricting (SGX_IOC_ENCLAVE_RESTRICT_PERMISSIONS)
  and one for permission relaxing (SGX_IOC_ENCLAVE_RELAX_PERMISSIONS)
  (Jarkko).
- In support of the ioctl() name change the following names have been
  changed:
  struct sgx_page_modp -> struct sgx_enclave_restrict_perm
  sgx_ioc_page_modp() -> sgx_ioc_enclave_restrict_perm()
  sgx_page_modp() -> sgx_enclave_restrict_perm()
- ioctl() takes entire secinfo as input instead of
  page permissions only (Jarkko).
- Fix kernel-doc to include () in function name.
- Create and use utility for the ETRACK flow.
- Fixups in comments
- Move kernel-doc to function that provides documentation for
  Documentation/x86/sgx.rst.
- Remove redundant comment.
- Make explicit which members of struct sgx_enclave_restrict_perm
  are for output (Dave).

 arch/x86/include/uapi/asm/sgx.h |  21 +++
 arch/x86/kernel/cpu/sgx/ioctl.c | 242 ++++++++++++++++++++++++++++++++
 2 files changed, 263 insertions(+)

diff --git a/arch/x86/include/uapi/asm/sgx.h b/arch/x86/include/uapi/asm/sgx.h
index f4b81587e90b..a0a24e94fb27 100644
--- a/arch/x86/include/uapi/asm/sgx.h
+++ b/arch/x86/include/uapi/asm/sgx.h
@@ -29,6 +29,8 @@ enum sgx_page_flags {
 	_IOW(SGX_MAGIC, 0x03, struct sgx_enclave_provision)
 #define SGX_IOC_VEPC_REMOVE_ALL \
 	_IO(SGX_MAGIC, 0x04)
+#define SGX_IOC_ENCLAVE_RESTRICT_PERMISSIONS \
+	_IOWR(SGX_MAGIC, 0x05, struct sgx_enclave_restrict_permissions)
 
 /**
  * struct sgx_enclave_create - parameter structure for the
@@ -76,6 +78,25 @@ struct sgx_enclave_provision {
 	__u64 fd;
 };
 
+/**
+ * struct sgx_enclave_restrict_permissions - parameters for ioctl
+ *                                        %SGX_IOC_ENCLAVE_RESTRICT_PERMISSIONS
+ * @offset:	starting page offset (page aligned relative to enclave base
+ *		address defined in SECS)
+ * @length:	length of memory (multiple of the page size)
+ * @secinfo:	address for the SECINFO data containing the new permission bits
+ *		for pages in range described by @offset and @length
+ * @result:	(output) SGX result code of ENCLS[EMODPR] function
+ * @count:	(output) bytes successfully changed (multiple of page size)
+ */
+struct sgx_enclave_restrict_permissions {
+	__u64 offset;
+	__u64 length;
+	__u64 secinfo;
+	__u64 result;
+	__u64 count;
+};
+
 struct sgx_enclave_run;
 
 /**
diff --git a/arch/x86/kernel/cpu/sgx/ioctl.c b/arch/x86/kernel/cpu/sgx/ioctl.c
index 0460fd224a05..4d88bfd163e7 100644
--- a/arch/x86/kernel/cpu/sgx/ioctl.c
+++ b/arch/x86/kernel/cpu/sgx/ioctl.c
@@ -660,6 +660,244 @@ static long sgx_ioc_enclave_provision(struct sgx_encl *encl, void __user *arg)
 	return sgx_set_attribute(&encl->attributes_mask, params.fd);
 }
 
+/*
+ * Ensure enclave is ready for SGX2 functions. Readiness is checked
+ * by ensuring the hardware supports SGX2 and the enclave is initialized
+ * and thus able to handle requests to modify pages within it.
+ */
+static int sgx_ioc_sgx2_ready(struct sgx_encl *encl)
+{
+	if (!(cpu_feature_enabled(X86_FEATURE_SGX2)))
+		return -ENODEV;
+
+	if (!test_bit(SGX_ENCL_INITIALIZED, &encl->flags))
+		return -EINVAL;
+
+	return 0;
+}
+
+/*
+ * Return valid permission fields from a secinfo structure provided by
+ * user space. The secinfo structure is required to only have bits in
+ * the permission fields set.
+ */
+static int sgx_perm_from_user_secinfo(void __user *_secinfo, u64 *secinfo_perm)
+{
+	struct sgx_secinfo secinfo;
+	u64 perm;
+
+	if (copy_from_user(&secinfo, (void __user *)_secinfo,
+			   sizeof(secinfo)))
+		return -EFAULT;
+
+	if (secinfo.flags & ~SGX_SECINFO_PERMISSION_MASK)
+		return -EINVAL;
+
+	if (memchr_inv(secinfo.reserved, 0, sizeof(secinfo.reserved)))
+		return -EINVAL;
+
+	perm = secinfo.flags & SGX_SECINFO_PERMISSION_MASK;
+
+	/*
+	 * Read access is required for the enclave to be able to use the page.
+	 * SGX instructions like ENCLU[EMODPE] and ENCLU[EACCEPT] require
+	 * read access.
+	 */
+	if (!(perm & SGX_SECINFO_R))
+		return -EINVAL;
+
+	*secinfo_perm = perm;
+
+	return 0;
+}
+
+/*
+ * Some SGX functions require that no cached linear-to-physical address
+ * mappings are present before they can succeed. Collaborate with
+ * hardware via ENCLS[ETRACK] to ensure that all cached
+ * linear-to-physical address mappings belonging to all threads of
+ * the enclave are cleared. See sgx_encl_cpumask() for details.
+ */
+static int sgx_enclave_etrack(struct sgx_encl *encl)
+{
+	void *epc_virt;
+	int ret;
+
+	epc_virt = sgx_get_epc_virt_addr(encl->secs.epc_page);
+	ret = __etrack(epc_virt);
+	if (ret) {
+		/*
+		 * ETRACK only fails when there is an OS issue. For
+		 * example, two consecutive ETRACK was sent without
+		 * completed IPI between.
+		 */
+		pr_err_once("ETRACK returned %d (0x%x)", ret, ret);
+		/*
+		 * Send IPIs to kick CPUs out of the enclave and
+		 * try ETRACK again.
+		 */
+		on_each_cpu_mask(sgx_encl_cpumask(encl), sgx_ipi_cb, NULL, 1);
+		ret = __etrack(epc_virt);
+		if (ret) {
+			pr_err_once("ETRACK repeat returned %d (0x%x)",
+				    ret, ret);
+			return -EFAULT;
+		}
+	}
+	on_each_cpu_mask(sgx_encl_cpumask(encl), sgx_ipi_cb, NULL, 1);
+
+	return 0;
+}
+
+/**
+ * sgx_enclave_restrict_permissions() - Restrict EPCM permissions
+ * @encl:	Enclave to which the pages belong.
+ * @modp:	Checked parameters from user on which pages need modifying.
+ * @secinfo_perm: New (validated) permission bits.
+ *
+ * Return:
+ * - 0:		Success.
+ * - -errno:	Otherwise.
+ */
+static long
+sgx_enclave_restrict_permissions(struct sgx_encl *encl,
+				 struct sgx_enclave_restrict_permissions *modp,
+				 u64 secinfo_perm)
+{
+	struct sgx_encl_page *entry;
+	struct sgx_secinfo secinfo;
+	unsigned long addr;
+	unsigned long c;
+	void *epc_virt;
+	int ret;
+
+	memset(&secinfo, 0, sizeof(secinfo));
+	secinfo.flags = secinfo_perm;
+
+	for (c = 0 ; c < modp->length; c += PAGE_SIZE) {
+		addr = encl->base + modp->offset + c;
+
+		mutex_lock(&encl->lock);
+
+		entry = sgx_encl_load_page(encl, addr);
+		if (IS_ERR(entry)) {
+			ret = PTR_ERR(entry) == -EBUSY ? -EAGAIN : -EFAULT;
+			goto out_unlock;
+		}
+
+		/*
+		 * Changing EPCM permissions is only supported on regular
+		 * SGX pages. Attempting this change on other pages will
+		 * result in #PF.
+		 */
+		if (entry->type != SGX_PAGE_TYPE_REG) {
+			ret = -EINVAL;
+			goto out_unlock;
+		}
+
+		/*
+		 * Do not verify the permission bits requested. Kernel
+		 * has no control over how EPCM permissions can be relaxed
+		 * from within the enclave. ENCLS[EMODPR] can only
+		 * remove existing EPCM permissions, attempting to set
+		 * new permissions will be ignored by the hardware.
+		 */
+
+		/* Change EPCM permissions. */
+		epc_virt = sgx_get_epc_virt_addr(entry->epc_page);
+		ret = __emodpr(&secinfo, epc_virt);
+		if (encls_faulted(ret)) {
+			/*
+			 * All possible faults should be avoidable:
+			 * parameters have been checked, will only change
+			 * permissions of a regular page, and no concurrent
+			 * SGX1/SGX2 ENCLS instructions since these
+			 * are protected with mutex.
+			 */
+			pr_err_once("EMODPR encountered exception %d\n",
+				    ENCLS_TRAPNR(ret));
+			ret = -EFAULT;
+			goto out_unlock;
+		}
+		if (encls_failed(ret)) {
+			modp->result = ret;
+			ret = -EFAULT;
+			goto out_unlock;
+		}
+
+		ret = sgx_enclave_etrack(encl);
+		if (ret) {
+			ret = -EFAULT;
+			goto out_unlock;
+		}
+
+		mutex_unlock(&encl->lock);
+	}
+
+	ret = 0;
+	goto out;
+
+out_unlock:
+	mutex_unlock(&encl->lock);
+out:
+	modp->count = c;
+
+	return ret;
+}
+
+/**
+ * sgx_ioc_enclave_restrict_permissions() - handler for
+ *                                        %SGX_IOC_ENCLAVE_RESTRICT_PERMISSIONS
+ * @encl:	an enclave pointer
+ * @arg:	userspace pointer to a &struct sgx_enclave_restrict_permissions
+ *		instance
+ *
+ * SGX2 distinguishes between relaxing and restricting the enclave page
+ * permissions maintained by the hardware (EPCM permissions) of pages
+ * belonging to an initialized enclave (after SGX_IOC_ENCLAVE_INIT).
+ *
+ * EPCM permissions cannot be restricted from within the enclave, the enclave
+ * requires the kernel to run the privileged level 0 instructions ENCLS[EMODPR]
+ * and ENCLS[ETRACK]. An attempt to relax EPCM permissions with this call
+ * will be ignored by the hardware.
+ *
+ * Return:
+ * - 0:		Success
+ * - -errno:	Otherwise
+ */
+static long sgx_ioc_enclave_restrict_permissions(struct sgx_encl *encl,
+						 void __user *arg)
+{
+	struct sgx_enclave_restrict_permissions params;
+	u64 secinfo_perm;
+	long ret;
+
+	ret = sgx_ioc_sgx2_ready(encl);
+	if (ret)
+		return ret;
+
+	if (copy_from_user(&params, arg, sizeof(params)))
+		return -EFAULT;
+
+	if (sgx_validate_offset_length(encl, params.offset, params.length))
+		return -EINVAL;
+
+	ret = sgx_perm_from_user_secinfo((void __user *)params.secinfo,
+					 &secinfo_perm);
+	if (ret)
+		return ret;
+
+	if (params.result || params.count)
+		return -EINVAL;
+
+	ret = sgx_enclave_restrict_permissions(encl, &params, secinfo_perm);
+
+	if (copy_to_user(arg, &params, sizeof(params)))
+		return -EFAULT;
+
+	return ret;
+}
+
 long sgx_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
 {
 	struct sgx_encl *encl = filep->private_data;
@@ -681,6 +919,10 @@ long sgx_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
 	case SGX_IOC_ENCLAVE_PROVISION:
 		ret = sgx_ioc_enclave_provision(encl, (void __user *)arg);
 		break;
+	case SGX_IOC_ENCLAVE_RESTRICT_PERMISSIONS:
+		ret = sgx_ioc_enclave_restrict_permissions(encl,
+							   (void __user *)arg);
+		break;
 	default:
 		ret = -ENOIOCTLCMD;
 		break;
-- 
2.25.1


  parent reply	other threads:[~2022-04-04 21:23 UTC|newest]

Thread overview: 79+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-04-04 16:49 [PATCH V3 00/30] x86/sgx and selftests/sgx: Support SGX2 Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 01/30] x86/sgx: Add short descriptions to ENCLS wrappers Reinette Chatre
2022-04-05  6:52   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 02/30] x86/sgx: Add wrapper for SGX2 EMODPR function Reinette Chatre
2022-04-05  6:53   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 03/30] x86/sgx: Add wrapper for SGX2 EMODT function Reinette Chatre
2022-04-05  6:53   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 04/30] x86/sgx: Add wrapper for SGX2 EAUG function Reinette Chatre
2022-04-05  6:54   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 05/30] x86/sgx: Support loading enclave page without VMA permissions check Reinette Chatre
2022-04-05  6:56   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 06/30] x86/sgx: Export sgx_encl_ewb_cpumask() Reinette Chatre
2022-04-05  6:56   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 07/30] x86/sgx: Rename sgx_encl_ewb_cpumask() as sgx_encl_cpumask() Reinette Chatre
2022-04-05  6:57   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 08/30] x86/sgx: Move PTE zap code to new sgx_zap_enclave_ptes() Reinette Chatre
2022-04-05  6:59   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 09/30] x86/sgx: Make sgx_ipi_cb() available internally Reinette Chatre
2022-04-05  6:59   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 10/30] x86/sgx: Create utility to validate user provided offset and length Reinette Chatre
2022-04-05  7:00   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 11/30] x86/sgx: Keep record of SGX page type Reinette Chatre
2022-04-05  7:00   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 12/30] x86/sgx: Export sgx_encl_{grow,shrink}() Reinette Chatre
2022-04-05  7:04   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 13/30] x86/sgx: Export sgx_encl_page_alloc() Reinette Chatre
2022-04-04 16:49 ` Reinette Chatre [this message]
2022-04-05  5:03   ` [PATCH V3 14/30] x86/sgx: Support restricting of enclave page permissions Jarkko Sakkinen
2022-04-05  5:07     ` Jarkko Sakkinen
2022-04-05 13:40       ` Jarkko Sakkinen
2022-04-05 14:19         ` Jarkko Sakkinen
2022-04-05 14:27           ` Jarkko Sakkinen
2022-04-05 14:52             ` Jarkko Sakkinen
2022-04-05 16:49               ` Reinette Chatre
2022-04-05 18:39                 ` Jarkko Sakkinen
2022-04-05 18:59                   ` Reinette Chatre
2022-04-06  7:30                     ` Jarkko Sakkinen
2022-04-06 17:51                       ` Reinette Chatre
2022-04-05 16:40             ` Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 15/30] x86/sgx: Support adding of pages to an initialized enclave Reinette Chatre
2022-04-05  5:05   ` Jarkko Sakkinen
2022-04-05 10:03     ` Jarkko Sakkinen
2022-04-06  7:37       ` Jarkko Sakkinen
2022-04-06 22:42         ` Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 16/30] x86/sgx: Tighten accessible memory range after enclave initialization Reinette Chatre
2022-04-05  7:05   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 17/30] x86/sgx: Support modifying SGX page type Reinette Chatre
2022-04-05  7:06   ` Jarkko Sakkinen
2022-04-05 15:34     ` Jarkko Sakkinen
2022-04-05 17:05       ` Reinette Chatre
2022-04-05 18:41         ` Jarkko Sakkinen
2022-04-05 18:59           ` Reinette Chatre
2022-04-06  7:32             ` Jarkko Sakkinen
2022-04-06 17:50               ` Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 18/30] x86/sgx: Support complete page removal Reinette Chatre
2022-04-05  7:08   ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 19/30] x86/sgx: Free up EPC pages directly to support large page ranges Reinette Chatre
2022-04-05  7:11   ` Jarkko Sakkinen
2022-04-05 17:13     ` Reinette Chatre
2022-04-05 17:25       ` Dave Hansen
2022-04-06  6:35         ` Jarkko Sakkinen
2022-04-06 17:50           ` Reinette Chatre
2022-04-05 18:42       ` Jarkko Sakkinen
2022-04-05 19:56         ` Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 20/30] Documentation/x86: Introduce enclave runtime management section Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 21/30] selftests/sgx: Add test for EPCM permission changes Reinette Chatre
2022-04-05  7:02   ` Jarkko Sakkinen
2022-04-05  7:03     ` Jarkko Sakkinen
2022-04-05 17:28     ` Reinette Chatre
2022-04-05 18:43       ` Jarkko Sakkinen
2022-04-04 16:49 ` [PATCH V3 22/30] selftests/sgx: Add test for TCS page " Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 23/30] selftests/sgx: Test two different SGX2 EAUG flows Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 24/30] selftests/sgx: Introduce dynamic entry point Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 25/30] selftests/sgx: Introduce TCS initialization enclave operation Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 26/30] selftests/sgx: Test complete changing of page type flow Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 27/30] selftests/sgx: Test faulty enclave behavior Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 28/30] selftests/sgx: Test invalid access to removed enclave page Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 29/30] selftests/sgx: Test reclaiming of untouched page Reinette Chatre
2022-04-04 16:49 ` [PATCH V3 30/30] selftests/sgx: Page removal stress test Reinette Chatre

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=8ed9ee98ca26c9eefde0fd49062bca6e7b9efe80.1648847675.git.reinette.chatre@intel.com \
    --to=reinette.chatre@intel.com \
    --cc=bp@alien8.de \
    --cc=cathy.zhang@intel.com \
    --cc=cedric.xing@intel.com \
    --cc=dave.hansen@linux.intel.com \
    --cc=haitao.huang@intel.com \
    --cc=hpa@zytor.com \
    --cc=jarkko@kernel.org \
    --cc=kai.huang@intel.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-sgx@vger.kernel.org \
    --cc=luto@kernel.org \
    --cc=mark.shanahan@intel.com \
    --cc=mingo@redhat.com \
    --cc=seanjc@google.com \
    --cc=tglx@linutronix.de \
    --cc=x86@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).