ipcz: Introduce parcel objects

This introduces parcel objects as a first-class concept of the public
ipcz API, replacing the concept of validators. In particular,
applications have the option to Get() parcel objects from portals
rather than getting the parcel's data and handles directly.

Data and handles can then be retrieved from a parcel object in the
same way they can be retrieved from portals, i.e. with the usual
Get/BeginGet/EndGet APIs.

This allows applications to consume individual parcel contents with
two-phase I/O operations (i.e. with direct access to the parcel
memory) without tying up the receiving portal in the meantime.

MojoIpcz exploits this new API feature to avoid copying parcel data
into its own type of MojoMessage objects, instead retaining a parcel
handle and exposing message data via a two-phase get.

Bug: 1299283
Fixed: 1384208
Change-Id: Iafd2efb16a1aa150dffb9baba9fe445ef01763e6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4023329
Commit-Queue: Ken Rockot <rockot@google.com>
Reviewed-by: Alex Gough <ajgo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1071963}
NOKEYCHECK=True
GitOrigin-RevId: da5cd04508573976a35a81780ef12f57bfc9bee9
diff --git a/include/ipcz/ipcz.h b/include/ipcz/ipcz.h
index 981fa42..e30cac0 100644
--- a/include/ipcz/ipcz.h
+++ b/include/ipcz/ipcz.h
@@ -250,6 +250,9 @@
   size_t region_num_bytes;
 };
 
+// IpczDriver
+// ==========
+//
 // IpczDriver is a function table to be populated by the application and
 // provided to ipcz when creating a new node. The driver implements concrete
 // I/O operations to facilitate communication between nodes, giving embedding
@@ -265,12 +268,18 @@
   // application before passing this structure to any ipcz API functions.
   size_t size;
 
+  // Close()
+  // =======
+  //
   // Called by ipcz to request that the driver release the object identified by
   // `handle`.
   IpczResult(IPCZ_API* Close)(IpczDriverHandle handle,  // in
                               uint32_t flags,           // in
                               const void* options);     // in
 
+  // Serialize()
+  // ===========
+  //
   // Serializes a driver object identified by `handle` into a collection of
   // bytes and readily transmissible driver objects, for eventual transmission
   // over `transport`. At a minimum this must support serialization of transport
@@ -327,6 +336,9 @@
                                   IpczDriverHandle* handles,   // out
                                   size_t* num_handles);        // in/out
 
+  // Deserialize()
+  // =============
+  //
   // Deserializes a driver object from a collection of bytes and transmissible
   // driver objects which which was originally produced by Serialize() and
   // received on the calling node via `transport`.
@@ -344,6 +356,9 @@
       const void* options,                     // in
       IpczDriverHandle* handle);               // out
 
+  // CreateTransports()
+  // ==================
+  //
   // Creates a new pair of entangled bidirectional transports, returning them in
   // `new_transport0` and `new_transport1`.
   //
@@ -364,6 +379,9 @@
       IpczDriverHandle* new_transport0,   // out
       IpczDriverHandle* new_transport1);  // out
 
+  // ActivateTransport()
+  // ===================
+  //
   // Called by ipcz to activate a transport. `driver_transport` is the
   // driver-side handle assigned to the transport by the driver, either as given
   // to ipcz via ConnectNode(), or as returned by the driver from an ipcz call
@@ -396,6 +414,9 @@
       uint32_t flags,                                 // in
       const void* options);                           // in
 
+  // DeactivateTransport()
+  // =====================
+  //
   // Called by ipcz to deactivate a transport. The driver does not need to
   // complete deactivation synchronously, but it must begin to deactivate the
   // transport and must invoke the transport's activity handler one final time
@@ -410,6 +431,9 @@
       uint32_t flags,                     // in
       const void* options);               // in
 
+  // Transmit()
+  // ==========
+  //
   // Called by ipcz to delegate transmission of data and driver handles over the
   // identified transport endpoint. If the driver cannot fulfill the request,
   // it must return a result other than IPCZ_RESULT_OK, and this will cause the
@@ -437,6 +461,9 @@
                                  uint32_t flags,                          // in
                                  const void* options);                    // in
 
+  // ReportBadTransportActivity()
+  // ============================
+  //
   // The ipcz Reject() API can be used by an application to reject a specific
   // parcel received from a portal. If the parcel in question came from a
   // remote node, ipcz invokes ReportBadTransportActivity() to notify the driver
@@ -449,6 +476,9 @@
                                                    uint32_t flags,
                                                    const void* options);
 
+  // AllocateSharedMemory()
+  // ======================
+  //
   // Allocates a shared memory region and returns a driver handle in
   // `driver_memory` which can be used to reference it in other calls to the
   // driver.
@@ -458,6 +488,9 @@
       const void* options,               // in
       IpczDriverHandle* driver_memory);  // out
 
+  // GetSharedMemoryInfo()
+  // =====================
+  //
   // Returns information about the shared memory region identified by
   // `driver_memory`.
   IpczResult(IPCZ_API* GetSharedMemoryInfo)(
@@ -466,6 +499,9 @@
       const void* options,                 // in
       struct IpczSharedMemoryInfo* info);  // out
 
+  // DuplicateSharedMemory()
+  // =======================
+  //
   // Duplicates a shared memory region handle into a new distinct handle
   // referencing the same underlying region.
   IpczResult(IPCZ_API* DuplicateSharedMemory)(
@@ -474,6 +510,9 @@
       const void* options,                   // in
       IpczDriverHandle* new_driver_memory);  // out
 
+  // MapSharedMemory()
+  // =================
+  //
   // Maps a shared memory region identified by `driver_memory` and returns its
   // mapped address in `address` on success and a driver handle in
   // `driver_mapping` which can be passed to the driver's Close() to unmap the
@@ -490,6 +529,9 @@
       void** address,                     // out
       IpczDriverHandle* driver_mapping);  // out
 
+  // GenerateRandomBytes()
+  // =====================
+  //
   // Generates `num_bytes` bytes of random data to fill `buffer`.
   IpczResult(IPCZ_API* GenerateRandomBytes)(size_t num_bytes,     // in
                                             uint32_t flags,       // in
@@ -639,9 +681,17 @@
 // a partial retrieval of the next available parcel. This means that in
 // situations where Get() would normally return IPCZ_RESULT_RESOURCE_EXHAUSTED,
 // it will instead return IPCZ_RESULT_OK with as much data and handles as the
-// caller indicated they could accept.
+// caller indicated they could accept. This flag may not be specified if
+// IPCZ_GET_PARCEL_ONLY is specified.
 #define IPCZ_GET_PARTIAL IPCZ_FLAG_BIT(0)
 
+// When given to Get() and a parcel is available to consume from the referenced
+// portal, no data or handles are consumed from the available parcel. Instead
+// only a handle to the parcel is returned, and the parcel is removed from the
+// portal to allow subsequent parcels to be retrieved. See documentation on
+// Get(). This flag may not be specified if IPCZ_GET_PARTIAL is specified.
+#define IPCZ_GET_PARCEL_ONLY IPCZ_FLAG_BIT(1)
+
 // See EndGet() and the IPCZ_END_GET_* flag descriptions below.
 typedef uint32_t IpczEndGetFlags;
 
@@ -824,6 +874,9 @@
 extern "C" {
 #endif
 
+// IpczAPI
+// =======
+//
 // Table of API functions defined by ipcz. Instances of this structure may be
 // populated by passing them to an implementation of IpczGetAPIFn.
 //
@@ -849,9 +902,13 @@
   // IpczGetAPIFn.
   size_t size;
 
+  // Close()
+  // =======
+  //
   // Releases the object identified by `handle`. If it's a portal, the portal is
-  // closed. If it's a node, the node is destroyed. If it's a wrapped driver
-  // object, the object is released via the driver API's Close().
+  // closed. If it's a node or parcel, the node or parcel is destroyed. If it's
+  // a wrapped driver object, the object is released via the driver API's
+  // Close().
   //
   // This function is NOT thread-safe. It is the application's responsibility to
   // ensure that no other threads are performing other operations on `handle`
@@ -881,6 +938,9 @@
                               uint32_t flags,        // in
                               const void* options);  // in
 
+  // CreateNode()
+  // ============
+  //
   // Initializes a new ipcz node. Applications typically need only one node in
   // each communicating process, but it's OK to create more. Practical use cases
   // for multiple nodes per process may include various testing scenarios, and
@@ -923,6 +983,9 @@
                                    const IpczCreateNodeOptions* options,  // in
                                    IpczHandle* node);                     // out
 
+  // ConnectNode()
+  // =============
+  //
   // Connects `node` to another node in the system using an application-provided
   // driver transport handle in `driver_transport` for communication. If this
   // call will succeed, ipcz will call back into the driver to activate this
@@ -995,6 +1058,9 @@
                                     const void* options,                // in
                                     IpczHandle* initial_portals);       // out
 
+  // OpenPortals()
+  // =============
+  //
   // Opens two new portals which exist as each other's opposite.
   //
   // Data and handles can be put in a portal with put operations (see Put(),
@@ -1024,6 +1090,9 @@
                                     IpczHandle* portal0,   // out
                                     IpczHandle* portal1);  // out
 
+  // MergePortals()
+  // ==============
+  //
   // Merges two portals into each other, effectively destroying both while
   // linking their respective peer portals with each other. A portal cannot
   // merge with its own peer, and a portal cannot be merged into another if one
@@ -1064,6 +1133,9 @@
                                      uint32_t flags,        // in
                                      const void* options);  // out
 
+  // QueryPortalStatus()
+  // ===================
+  //
   // Queries specific details regarding the status of a portal, such as the
   // number of unread parcels or data bytes available on the portal or its
   // opposite, or whether the opposite portal has already been closed.
@@ -1090,6 +1162,9 @@
       const void* options,               // in
       struct IpczPortalStatus* status);  // out
 
+  // Put()
+  // =====
+  //
   // Puts any combination of data and handles into the portal identified by
   // `portal`. Everything put into a portal can be retrieved in the same order
   // by a corresponding get operation on the opposite portal. Depending on the
@@ -1139,6 +1214,9 @@
                             uint32_t flags,                         // in
                             const struct IpczPutOptions* options);  // in
 
+  // BeginPut()
+  // ==========
+  //
   // Begins a two-phase put operation on `portal`. While a two-phase put
   // operation is in progress on a portal, any other BeginPut() call on the same
   // portal will fail with IPCZ_RESULT_ALREADY_EXISTS.
@@ -1193,6 +1271,9 @@
       size_t* num_bytes,                          // out
       void** data);                               // out
 
+  // EndPut()
+  // ========
+  //
   // Ends the two-phase put operation started by the most recent successful call
   // to BeginPut() on `portal`.
   //
@@ -1240,18 +1321,29 @@
                                IpczEndPutFlags flags,      // in
                                const void* options);       // in
 
-  // Retrieves some combination of data and handles from a portal, as placed by
-  // a prior put operation on the opposite portal.
+  // Get()
+  // =====
   //
-  // On input, the values pointed to by `num_bytes` and `num_handles` must
-  // specify the capacity of each corresponding buffer argument. A null pointer
-  // implies zero capacity. It is an error to specify non-zero capacity if the
-  // corresponding buffer (`data` or `handles`) is null.
+  // Retrieves some combination of data and handles from a source object.
+  //
+  // If IPCZ_GET_PARCEL_ONLY is specified in `flags` and `source` is a portal,
+  // then `data`, `num_bytes` `handles`, and `num_handles` are all ignored and,
+  // if a parcel is available to retrieve from the portal, a handle to it is
+  // output in `parcel`. This handle can itself be used with Get() (or
+  // BeginGet() and EndGet()) to retrieve the parcel's contents, or with
+  // Reject() to reject its contents. Returned parcels are owned by the caller
+  // and must eventually be closed with Close() to release any associated
+  // resources.
+  //
+  // Otherwise, on input the values pointed to by `num_bytes` and `num_handles`
+  // must specify the capacity of each corresponding buffer argument. A null
+  // pointer implies zero capacity. It is an error to specify non-zero capacity
+  // if the corresponding buffer (`data` or `handles`) is null.
   //
   // Normally the data consumed by this call is copied directly to the address
   // given by the `data` argument, and `*num_bytes` specifies how many bytes of
   // storage are available there.  If an application wishes to read directly
-  // from portal memory instead, a two-phase get operation can be used by
+  // from parcel memory instead, a two-phase get operation can be used by
   // calling BeginGet() and EndGet() as defined below.
   //
   // Note that if the caller does not provide enough storage capacity for a
@@ -1260,70 +1352,85 @@
   // required for the message without copying any of its contents. See details
   // of that return value below.
   //
-  // If IPCZ_GET_PARTIAL is specified, the call succeeds as long as a parcel is
-  // available, and the caller retrieves as much data and handles as their
-  // expressed capacity will allow. In this case, the in/out capacity arguments
-  // (`num_bytes` and `num_handles`) are still updated as specified in the
-  // IPCZ_RESULT_OK details below.
+  // If IPCZ_GET_PARTIAL is specified, the call succeeds as long as `source` is
+  // a parcel or a portal with a parcel available. In this case the caller
+  // retrieves as much data and handles as their expressed capacity will allow,
+  // and the in/out capacity arguments (`num_bytes` and `num_handles`) are still
+  // updated as specified in the IPCZ_RESULT_OK details below.
   //
-  // If this call succeeds and `validator` is non-null, it's populated with a
-  // new validator handle which the application can use to report
-  // application-level validation failures regarding this specific transaction.
-  // See Reject().
+  // If this call succeeds and `parcel` is non-null, then `*parcel` is populated
+  // with a new parcel handle which the application can use to report
+  // application-level validation failures regarding the retreived parcel (see
+  // Reject()).
   //
   // `options` is ignored and must be null.
   //
   // Returns:
   //
-  //    IPCZ_RESULT_OK if there was a parcel available in the portal's queue and
-  //        its data and handles were able to be copied into the caller's
-  //        provided buffers. In this case values pointed to by `num_bytes` and
-  //        `num_handles` (for each one that is non-null) are updated to reflect
-  //        what was actually consumed. Note that the caller assumes ownership
-  //        of all returned handles.
+  //    IPCZ_RESULT_OK if `source` is a portal and there is a parcel available
+  //        in the portal's queue, or `source` is a parcel; and in either case
+  //        the parcel's data and handles were able to be copied into the
+  //        caller's provided buffers, or IPCZ_GET_PARCEL_ONLY was specified and
+  //        `parcel` was non-null.
   //
-  //    IPCZ_RESULT_INVALID_ARGUMENT if `portal` is invalid, `data` is null but
-  //        `*num_bytes` is non-zero, or `handles` is null but `*num_handles` is
-  //        non-zero.
+  //        When IPCZ_GET_PARCEL_ONLY is not specified, values pointed to by
+  //        `num_bytes` and `num_handles` (for each one that is non-null) are
+  //        updated to reflect what was actually consumed. Note that the caller
+  //        assumes ownership of all returned handles.
   //
-  //    IPCZ_RESULT_RESOURCE_EXHAUSTED if the next available parcel would exceed
-  //        the caller's specified capacity for either data bytes or handles,
-  //        and IPCZ_GET_PARTIAL was not specified in `flags`. In this case, any
+  //        If `parcel` was non-null, it is populated with a handle to the
+  //        retrieved parcel object. If any attached handles were consumed by
+  //        the Get() call itself, they will no longer be attached to the
+  //        returned parcel object.
+  //
+  //    IPCZ_RESULT_INVALID_ARGUMENT if `source` is invalid or not a portal or
+  //        parcel, `data` is null but `*num_bytes` is non-zero, `handles` is
+  //        null but `*num_handles` is non-zero; IPCZ_GET_PARCEL_ONLY is
+  //        specified in `flags` but `parcel` is null; `parcel` is non-null but
+  //        `source` is itself a parcel; or `parcel` is non-null but
+  //        IPCZ_GET_PARTIAL is specified in `flags`.
+  //
+  //    IPCZ_RESULT_RESOURCE_EXHAUSTED if the consumed parcel would exceed the
+  //        caller's specified capacity for either data bytes or handles, and
+  //        IPCZ_GET_PARTIAL was not specified in `flags`. In this case any
   //        non-null size pointer is updated to convey the minimum capacity that
   //        would have been required for an otherwise identical Get() call to
   //        have succeeded. Callers observing this result may wish to allocate
   //        storage accordingly and retry with updated parameters.
   //
-  //    IPCZ_RESULT_UNAVAILABLE if the portal's parcel queue is currently empty.
-  //        In this case callers should wait before attempting to get anything
-  //        from the same portal again.
+  //    IPCZ_RESULT_UNAVAILABLE if `source` is a portal whose parcel queue is
+  //        currently empty. In this case callers should wait before attempting
+  //        to get anything from the same portal again.
   //
-  //    IPCZ_RESULT_NOT_FOUND if there are no more parcels in the portal's queue
-  //        AND the opposite portal is known to be closed. If this result is
-  //        returned, no parcels can ever be read from this portal again.
+  //    IPCZ_RESULT_NOT_FOUND if `source` is a portal which has no more parcels
+  //        in its queue and whose peer portal is known to be closed. If this
+  //        result is returned, no more parcels can ever be read from `source`.
   //
   //    IPCZ_RESULT_ALREADY_EXISTS if there is a two-phase get operation in
-  //        progress on `portal`.
-  IpczResult(IPCZ_API* Get)(IpczHandle portal,       // in
-                            IpczGetFlags flags,      // in
-                            const void* options,     // in
-                            void* data,              // out
-                            size_t* num_bytes,       // in/out
-                            IpczHandle* handles,     // out
-                            size_t* num_handles,     // in/out
-                            IpczHandle* validator);  // out
+  //        progress on `source`.
+  IpczResult(IPCZ_API* Get)(IpczHandle source,    // in
+                            IpczGetFlags flags,   // in
+                            const void* options,  // in
+                            void* data,           // out
+                            size_t* num_bytes,    // in/out
+                            IpczHandle* handles,  // out
+                            size_t* num_handles,  // in/out
+                            IpczHandle* parcel);  // out
 
-  // Begins a two-phase get operation on `portal` to retrieve data and handles.
-  // While a two-phase get operation is in progress on a portal, all other get
-  // operations on the same portal will fail with IPCZ_RESULT_ALREADY_EXISTS.
+  // BeginGet()
+  // ==========
+  //
+  // Begins a two-phase get operation on `source` to retrieve data and handles.
+  // While a two-phase get operation is in progress on an object, all other get
+  // operations on the same object will fail with IPCZ_RESULT_ALREADY_EXISTS.
   //
   // Unlike a plain Get() call, two-phase get operations allow the application
-  // to read directly from portal memory, potentially reducing memory access
+  // to read directly from parcel memory, potentially reducing memory access
   // costs by eliminating redundant copying and caching.
   //
-  // If `data` or `num_bytes` is null and the available parcel has at least one
-  // byte of data, or if there are handles present but `num_handles` is null,
-  // this returns IPCZ_RESULT_RESOURCE_EXHAUSTED.
+  // If `data` or `num_bytes` is null and the parcel has at least one byte of
+  // data, or if there are handles present but `num_handles` is null, this
+  // returns IPCZ_RESULT_RESOURCE_EXHAUSTED.
   //
   // Otherwise a successful BeginGet() updates values pointed to by `data`,
   // `num_bytes`, and `num_handles` to convey the parcel's data storage and
@@ -1331,10 +1438,8 @@
   //
   // NOTE: When performing two-phase get operations, callers should be mindful
   // of time-of-check/time-of-use (TOCTOU) vulnerabilities. Exposed parcel
-  // memory may be shared with (and writable in) the process which placed the
-  // parcel into the portal, and that process may not be trustworthy. In such
-  // cases, applications should be careful to copy the data out before
-  // validating and using it.
+  // memory may be shared with (and writable in) the process which transmitted
+  // the parcel, and that process may not be trustworthy.
   //
   // `flags` is ignored and must be IPCZ_NO_FLAGS.
   //
@@ -1344,36 +1449,39 @@
   //
   //    IPCZ_RESULT_OK if the two-phase get was successfully initiated. In this
   //        case both `*data` and `*num_bytes` are updated (if `data` and
-  //        `num_bytes` were non-null) to describe the portal memory from which
+  //        `num_bytes` were non-null) to describe the parcel memory from which
   //        the application is free to read parcel data. If `num_handles` is
   //        is non-null, the value pointed to is updated to reflect the number
   //        of handles available to retrieve.
   //
-  //    IPCZ_RESULT_INVALID_ARGUMENT if `portal` is invalid.
+  //    IPCZ_RESULT_INVALID_ARGUMENT if `source` is invalid.
   //
-  //    IPCZ_RESULT_RESOURCE_EXHAUSTED if the next available parcel has at least
-  //        one data byte but `data` or `num_bytes` is null; or if the parcel
-  //        has any handles but `num_handles` null.
+  //    IPCZ_RESULT_RESOURCE_EXHAUSTED if the parcel has at least one data byte
+  //        but `data` or `num_bytes` is null; or if the parcel has any handles
+  //        but `num_handles` null.
   //
-  //    IPCZ_RESULT_UNAVAILABLE if the portal's parcel queue is currently empty.
-  //        In this case callers should wait before attempting to get anything
-  //        from the same portal again.
+  //    IPCZ_RESULT_UNAVAILABLE if `source` is a portal whose parcel queue is
+  //        currently empty. In this case callers should wait before attempting
+  //        to get anything from the same portal again.
   //
-  //    IPCZ_RESULT_NOT_FOUND if there are no more parcels in the portal's queue
-  //        AND the opposite portal is known to be closed. In this case, no get
-  //        operation can ever succeed again on this portal.
+  //    IPCZ_RESULT_NOT_FOUND if `source` is a portal with no more parcels in
+  //        its queue and whose peer portal is known to be closed. In this case,
+  //        no get operation can ever succeed again on this portal.
   //
   //    IPCZ_RESULT_ALREADY_EXISTS if there is already a two-phase get operation
-  //        in progress on `portal`.
-  IpczResult(IPCZ_API* BeginGet)(IpczHandle portal,     // in
+  //        in progress on `source`.
+  IpczResult(IPCZ_API* BeginGet)(IpczHandle source,     // in
                                  uint32_t flags,        // in
                                  const void* options,   // in
                                  const void** data,     // out
                                  size_t* num_bytes,     // out
                                  size_t* num_handles);  // out
 
+  // EndGet()
+  // ========
+  //
   // Ends the two-phase get operation started by the most recent successful call
-  // to BeginGet() on `portal`.
+  // to BeginGet() on `source`.
   //
   // `num_bytes_consumed` specifies the number of bytes actually read from the
   // buffer that was returned from the original BeginGet() call. `num_handles`
@@ -1381,40 +1489,38 @@
   // capacity indicated by the corresponding output from BeginGet().
   //
   // If IPCZ_END_GET_ABORT is given in `flags` and there is a two-phase get
-  // operation in progress on `portal`, all other arguments are ignored and the
-  // pending operation is cancelled without consuming any data from the portal.
-  //
-  // If this call succeeds (without IPCZ_END_GET_ABORT specified) and
-  // `validator` is non-null, it's populated with a new validator handle which
-  // the application can use to report application-level validation failures
-  // regarding this specific transaction. See Reject().
+  // operation in progress on `source`, all other arguments are ignored and the
+  // pending operation is cancelled without consuming any data from the source.
   //
   // `options` is unused and must be null.
   //
   // Returns:
   //
   //    IPCZ_RESULT_OK if the two-phase operation was successfully completed or
-  //        aborted. Note that if the frontmost parcel wasn't fully consumed by
-  //        the caller, it will remain in queue with the rest of its data intact
-  //        for a subsequent get operation to retrieve. Exactly `num_handles`
-  //        handles will be copied into `handles`.
+  //        aborted. Note that if `source` is a portal and its frontmost parcel
+  //        was not fully consumed by this call, it will remain in queue with
+  //        the rest of its data intact for a subsequent get operation to
+  //        retrieve from the portal. Exactly `num_handles` handles will be
+  //        copied into `handles`.
   //
-  //    IPCZ_RESULT_INVALID_ARGUMENT if `portal` is invalid, or if `num_handles`
+  //    IPCZ_RESULT_INVALID_ARGUMENT if `source` is invalid, or if `num_handles`
   //        is non-zero but `handles` is null.
   //
   //    IPCZ_RESULT_OUT_OF_RANGE if either `num_bytes_consumed` or `num_handles`
   //        is larger than the capacity returned by BeginGet().
   //
   //    IPCZ_RESULT_FAILED_PRECONDITION if there was no two-phase get operation
-  //        in progress on `portal`.
-  IpczResult(IPCZ_API* EndGet)(IpczHandle portal,          // in
+  //        in progress on `source`.
+  IpczResult(IPCZ_API* EndGet)(IpczHandle source,          // in
                                size_t num_bytes_consumed,  // in
                                size_t num_handles,         // in
                                IpczEndGetFlags flags,      // in
                                const void* options,        // in
-                               IpczHandle* handles,        // out
-                               IpczHandle* validator);     // out
+                               IpczHandle* handles);       // out
 
+  // Trap()
+  // ======
+  //
   // Attempts to install a trap to catch interesting changes to a portal's
   // state. The condition(s) to observe are specified in `conditions`.
   // Regardless of what conditions the caller specifies, all successfully
@@ -1471,8 +1577,12 @@
       IpczTrapConditionFlags* satisfied_condition_flags,  // out
       struct IpczPortalStatus* status);                   // out
 
+  // Reject()
+  // ========
+  //
   // Reports an application-level validation failure to ipcz, in reference to
-  // a specific `validator` returned by a previous call to Get() or EndGet().
+  // a specific `parcel` returned by a previous call to Get().
+  //
   // ipcz propagates this rejection to the driver via
   // ReportBadTransportActivity(), if and only if the associated parcel did in
   // fact come from a remote node.
@@ -1489,16 +1599,19 @@
   //    IPCZ_RESULT_OK if the driver was successfully notified about this
   //        rejection via ReportBadTransportActivity().
   //
-  //    IPCZ_RESULT_INVALID_ARGUMENT if `validator` is not a valid validator
-  //        handle previously returned by Get() or EndGet().
+  //    IPCZ_RESULT_INVALID_ARGUMENT if `parcel` is not a valid parcel handle
+  //        previously returned by Get().
   //
-  //    IPCZ_RESULT_FAILED_PRECONDITION if `validator` is associated with a
-  //        parcel that did not come from another node.
-  IpczResult(IPCZ_API* Reject)(IpczHandle validator,
+  //    IPCZ_RESULT_FAILED_PRECONDITION if `parcel` is associated with a parcel
+  //        that did not come from another node.
+  IpczResult(IPCZ_API* Reject)(IpczHandle parcel,
                                uintptr_t context,
                                uint32_t flags,
                                const void* options);
 
+  // Box()
+  // =====
+  //
   // Boxes an object managed by a node's driver and returns a new IpczHandle to
   // reference the box. If the driver is able to serialize the boxed object, the
   // box can be placed into a portal for transmission to the other side.
@@ -1522,6 +1635,9 @@
                             const void* options,             // in
                             IpczHandle* handle);             // out
 
+  // Unbox()
+  // =======
+  //
   // Unboxes a driver object from an IpczHandle previously produced by Box().
   //
   // `flags` is ignored and must be 0.
diff --git a/src/BUILD.gn b/src/BUILD.gn
index 8b98d75..a8fe1da 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -232,6 +232,7 @@
     "ipcz/operation_context.h",
     "ipcz/parcel.h",
     "ipcz/parcel_queue.h",
+    "ipcz/parcel_wrapper.h",
     "ipcz/portal.h",
     "ipcz/ref_counted_fragment.h",
     "ipcz/remote_router_link.h",
@@ -243,7 +244,6 @@
     "ipcz/sequenced_queue.h",
     "ipcz/sublink_id.h",
     "ipcz/test_messages.h",
-    "ipcz/validator.h",
   ]
   sources = [
     "ipcz/api_object.cc",
@@ -285,6 +285,7 @@
     "ipcz/node_name.cc",
     "ipcz/parcel.cc",
     "ipcz/parcel_queue.cc",
+    "ipcz/parcel_wrapper.cc",
     "ipcz/portal.cc",
     "ipcz/ref_counted_fragment.cc",
     "ipcz/remote_router_link.cc",
@@ -299,7 +300,6 @@
     "ipcz/trap_event_dispatcher.h",
     "ipcz/trap_set.cc",
     "ipcz/trap_set.h",
-    "ipcz/validator.cc",
   ]
   public_deps = [
     ":ipcz_header",
@@ -387,6 +387,7 @@
     "ipcz/router_link_test.cc",
     "ipcz/sequenced_queue_test.cc",
     "merge_portals_test.cc",
+    "parcel_test.cc",
     "queueing_test.cc",
     "reference_drivers/sync_reference_driver_test.cc",
     "remote_portal_test.cc",
diff --git a/src/api.cc b/src/api.cc
index 061d849..756648d 100644
--- a/src/api.cc
+++ b/src/api.cc
@@ -11,9 +11,9 @@
 #include "ipcz/ipcz.h"
 #include "ipcz/node.h"
 #include "ipcz/node_link_memory.h"
+#include "ipcz/parcel_wrapper.h"
 #include "ipcz/portal.h"
 #include "ipcz/router.h"
-#include "ipcz/validator.h"
 #include "util/ref_counted.h"
 
 extern "C" {
@@ -214,56 +214,80 @@
                            absl::MakeSpan(handles, num_handles));
 }
 
-IpczResult Get(IpczHandle portal_handle,
+IpczResult Get(IpczHandle source,
                IpczGetFlags flags,
                const void* options,
                void* data,
                size_t* num_bytes,
                IpczHandle* handles,
                size_t* num_handles,
-               IpczHandle* validator) {
-  ipcz::Portal* portal = ipcz::Portal::FromHandle(portal_handle);
-  if (!portal) {
+               IpczHandle* parcel) {
+  if ((flags & IPCZ_GET_PARTIAL) && parcel) {
     return IPCZ_RESULT_INVALID_ARGUMENT;
   }
-  return portal->Get(flags, data, num_bytes, handles, num_handles, validator);
+
+  if (ipcz::Portal* portal = ipcz::Portal::FromHandle(source)) {
+    if ((flags & IPCZ_GET_PARCEL_ONLY) && !parcel) {
+      return IPCZ_RESULT_INVALID_ARGUMENT;
+    }
+
+    return portal->Get(flags, data, num_bytes, handles, num_handles, parcel);
+  }
+
+  if (ipcz::ParcelWrapper* wrapper = ipcz::ParcelWrapper::FromHandle(source)) {
+    if ((flags & IPCZ_GET_PARCEL_ONLY) || parcel) {
+      return IPCZ_RESULT_INVALID_ARGUMENT;
+    }
+    return wrapper->Get(flags, data, num_bytes, handles, num_handles);
+  }
+
+  return IPCZ_RESULT_INVALID_ARGUMENT;
 }
 
-IpczResult BeginGet(IpczHandle portal_handle,
+IpczResult BeginGet(IpczHandle source,
                     uint32_t flags,
                     const void* options,
                     const void** data,
                     size_t* num_bytes,
                     size_t* num_handles) {
-  ipcz::Portal* portal = ipcz::Portal::FromHandle(portal_handle);
-  if (!portal) {
-    return IPCZ_RESULT_INVALID_ARGUMENT;
+  if (ipcz::Portal* portal = ipcz::Portal::FromHandle(source)) {
+    return portal->BeginGet(data, num_bytes, num_handles);
   }
 
-  return portal->BeginGet(data, num_bytes, num_handles);
+  if (ipcz::ParcelWrapper* parcel = ipcz::ParcelWrapper::FromHandle(source)) {
+    return parcel->BeginGet(data, num_bytes, num_handles);
+  }
+
+  return IPCZ_RESULT_INVALID_ARGUMENT;
 }
 
-IpczResult EndGet(IpczHandle portal_handle,
+IpczResult EndGet(IpczHandle source,
                   size_t num_bytes_consumed,
                   size_t num_handles,
                   IpczEndGetFlags flags,
                   const void* options,
-                  IpczHandle* handles,
-                  IpczHandle* validator) {
-  ipcz::Portal* portal = ipcz::Portal::FromHandle(portal_handle);
-  if (!portal) {
-    return IPCZ_RESULT_INVALID_ARGUMENT;
-  }
+                  IpczHandle* handles) {
   if (num_handles > 0 && !handles) {
     return IPCZ_RESULT_INVALID_ARGUMENT;
   }
 
-  if (flags & IPCZ_END_GET_ABORT) {
-    return portal->AbortGet();
+  if (ipcz::Portal* portal = ipcz::Portal::FromHandle(source)) {
+    if (flags & IPCZ_END_GET_ABORT) {
+      return portal->AbortGet();
+    }
+    return portal->CommitGet(num_bytes_consumed,
+                             absl::MakeSpan(handles, num_handles));
   }
 
-  return portal->CommitGet(num_bytes_consumed,
-                           absl::MakeSpan(handles, num_handles), validator);
+  if (ipcz::ParcelWrapper* parcel = ipcz::ParcelWrapper::FromHandle(source)) {
+    if (flags & IPCZ_END_GET_ABORT) {
+      return parcel->AbortGet();
+    }
+    return parcel->CommitGet(num_bytes_consumed,
+                             absl::MakeSpan(handles, num_handles));
+  }
+
+  return IPCZ_RESULT_INVALID_ARGUMENT;
 }
 
 IpczResult Trap(IpczHandle portal_handle,
@@ -288,16 +312,16 @@
                                 satisfied_condition_flags, status);
 }
 
-IpczResult Reject(IpczHandle validator_handle,
+IpczResult Reject(IpczHandle parcel_handle,
                   uintptr_t context,
                   uint32_t flags,
                   const void* options) {
-  ipcz::Validator* validator = ipcz::Validator::FromHandle(validator_handle);
-  if (!validator) {
+  ipcz::ParcelWrapper* parcel = ipcz::ParcelWrapper::FromHandle(parcel_handle);
+  if (!parcel) {
     return IPCZ_RESULT_INVALID_ARGUMENT;
   }
 
-  return validator->Reject(context);
+  return parcel->Reject(context);
 }
 
 IpczResult Box(IpczHandle node_handle,
diff --git a/src/api_test.cc b/src/api_test.cc
index 88275cf..ae80bf5 100644
--- a/src/api_test.cc
+++ b/src/api_test.cc
@@ -269,6 +269,11 @@
                        nullptr, nullptr));
   EXPECT_EQ(2u, num_bytes);
 
+  // Invalid arguments: null data but non-zero data capacity.
+  EXPECT_EQ(IPCZ_RESULT_INVALID_ARGUMENT,
+            ipcz().Get(b, IPCZ_NO_FLAGS, nullptr, nullptr, &num_bytes, nullptr,
+                       nullptr, nullptr));
+
   num_bytes = 4;
   EXPECT_EQ(IPCZ_RESULT_OK, ipcz().Get(b, IPCZ_NO_FLAGS, nullptr, data,
                                        &num_bytes, nullptr, nullptr, nullptr));
@@ -299,7 +304,12 @@
             ipcz().Get(b, IPCZ_NO_FLAGS, nullptr, nullptr, nullptr, nullptr,
                        nullptr, nullptr));
 
+  // Invalid arguments: null handles but non-zero handle capacity.
   size_t num_handles = 1;
+  EXPECT_EQ(IPCZ_RESULT_INVALID_ARGUMENT,
+            ipcz().Get(b, IPCZ_NO_FLAGS, nullptr, nullptr, nullptr, nullptr,
+                       &num_handles, nullptr));
+
   EXPECT_EQ(IPCZ_RESULT_OK, ipcz().Get(b, IPCZ_NO_FLAGS, nullptr, nullptr,
                                        nullptr, &d, &num_handles, nullptr));
   EXPECT_EQ(1u, num_handles);
@@ -405,23 +415,22 @@
   // Invalid handle.
   EXPECT_EQ(IPCZ_RESULT_INVALID_ARGUMENT,
             ipcz().EndGet(IPCZ_INVALID_HANDLE, 0, 0, IPCZ_NO_FLAGS, nullptr,
-                          nullptr, nullptr));
+                          nullptr));
 
   // Non-zero handle count with null handle buffer.
   EXPECT_EQ(IPCZ_RESULT_INVALID_ARGUMENT,
-            ipcz().EndGet(a, 0, 1, IPCZ_NO_FLAGS, nullptr, nullptr, nullptr));
+            ipcz().EndGet(a, 0, 1, IPCZ_NO_FLAGS, nullptr, nullptr));
 
   // Data size out of range.
-  EXPECT_EQ(IPCZ_RESULT_OUT_OF_RANGE,
-            ipcz().EndGet(a, num_bytes + 1, 0, IPCZ_NO_FLAGS, nullptr, nullptr,
-                          nullptr));
+  EXPECT_EQ(
+      IPCZ_RESULT_OUT_OF_RANGE,
+      ipcz().EndGet(a, num_bytes + 1, 0, IPCZ_NO_FLAGS, nullptr, nullptr));
 
   // Two-phase Get not in progress.
-  EXPECT_EQ(IPCZ_RESULT_OK, ipcz().EndGet(a, num_bytes, 0, IPCZ_NO_FLAGS,
-                                          nullptr, nullptr, nullptr));
-  EXPECT_EQ(
-      IPCZ_RESULT_FAILED_PRECONDITION,
-      ipcz().EndGet(a, num_bytes, 0, IPCZ_NO_FLAGS, nullptr, nullptr, nullptr));
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().EndGet(a, num_bytes, 0, IPCZ_NO_FLAGS, nullptr, nullptr));
+  EXPECT_EQ(IPCZ_RESULT_FAILED_PRECONDITION,
+            ipcz().EndGet(a, num_bytes, 0, IPCZ_NO_FLAGS, nullptr, nullptr));
 
   CloseAll({a, b, node});
 }
@@ -446,15 +455,15 @@
   EXPECT_EQ(kMessage[0], *reinterpret_cast<const char*>(in_data));
 
   EXPECT_EQ(IPCZ_RESULT_OK,
-            ipcz().EndGet(b, 1, 0, IPCZ_NO_FLAGS, nullptr, nullptr, nullptr));
+            ipcz().EndGet(b, 1, 0, IPCZ_NO_FLAGS, nullptr, nullptr));
 
   EXPECT_EQ(IPCZ_RESULT_OK, ipcz().BeginGet(b, IPCZ_NO_FLAGS, nullptr, &in_data,
                                             &num_bytes, nullptr));
   EXPECT_EQ(
       kMessage.substr(1),
       std::string_view(reinterpret_cast<const char*>(in_data), num_bytes));
-  EXPECT_EQ(IPCZ_RESULT_OK, ipcz().EndGet(b, num_bytes, 0, IPCZ_NO_FLAGS,
-                                          nullptr, nullptr, nullptr));
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().EndGet(b, num_bytes, 0, IPCZ_NO_FLAGS, nullptr, nullptr));
 
   EXPECT_EQ(
       IPCZ_RESULT_UNAVAILABLE,
@@ -521,16 +530,15 @@
 
   char byte;
   size_t num_bytes = 1;
-  IpczHandle validator;
-  EXPECT_EQ(IPCZ_RESULT_OK,
-            ipcz().Get(b, IPCZ_NO_FLAGS, nullptr, &byte, &num_bytes, nullptr,
-                       nullptr, &validator));
+  IpczHandle parcel;
+  EXPECT_EQ(IPCZ_RESULT_OK, ipcz().Get(b, IPCZ_NO_FLAGS, nullptr, &byte,
+                                       &num_bytes, nullptr, nullptr, &parcel));
   EXPECT_EQ('!', byte);
 
   EXPECT_EQ(IPCZ_RESULT_FAILED_PRECONDITION,
-            ipcz().Reject(validator, 0, IPCZ_NO_FLAGS, nullptr));
+            ipcz().Reject(parcel, 0, IPCZ_NO_FLAGS, nullptr));
 
-  CloseAll({a, b, node, validator});
+  CloseAll({a, b, node, parcel});
 }
 
 TEST_F(APITest, RejectRemote) {
@@ -554,10 +562,9 @@
   Put(a, "!");
   char byte;
   size_t num_bytes = 1;
-  IpczHandle validator;
-  EXPECT_EQ(IPCZ_RESULT_OK,
-            ipcz().Get(b, IPCZ_NO_FLAGS, nullptr, &byte, &num_bytes, nullptr,
-                       nullptr, &validator));
+  IpczHandle parcel;
+  EXPECT_EQ(IPCZ_RESULT_OK, ipcz().Get(b, IPCZ_NO_FLAGS, nullptr, &byte,
+                                       &num_bytes, nullptr, nullptr, &parcel));
   EXPECT_EQ('!', byte);
 
   constexpr uintptr_t kTestContext = 42;
@@ -569,12 +576,12 @@
         error_context = context;
       });
   EXPECT_EQ(IPCZ_RESULT_OK,
-            ipcz().Reject(validator, kTestContext, IPCZ_NO_FLAGS, nullptr));
+            ipcz().Reject(parcel, kTestContext, IPCZ_NO_FLAGS, nullptr));
   EXPECT_EQ(transport1, error_transport);
   EXPECT_EQ(kTestContext, error_context);
   reference_drivers::SetBadTransportActivityCallback(nullptr);
 
-  CloseAll({a, b, node_b, node_a, validator});
+  CloseAll({a, b, node_b, node_a, parcel});
 }
 
 TEST_F(APITest, BoxInvalid) {
diff --git a/src/ipcz/api_object.h b/src/ipcz/api_object.h
index 0477ccd..20509ab 100644
--- a/src/ipcz/api_object.h
+++ b/src/ipcz/api_object.h
@@ -24,7 +24,7 @@
     kPortal,
     kBox,
     kTransport,
-    kValidator,
+    kParcel,
   };
 
   explicit APIObject(ObjectType type);
diff --git a/src/ipcz/parcel_wrapper.cc b/src/ipcz/parcel_wrapper.cc
new file mode 100644
index 0000000..6b740f1
--- /dev/null
+++ b/src/ipcz/parcel_wrapper.cc
@@ -0,0 +1,127 @@
+// Copyright 2022 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ipcz/parcel_wrapper.h"
+
+#include "ipcz/driver_object.h"
+#include "ipcz/driver_transport.h"
+#include "ipcz/ipcz.h"
+#include "ipcz/node.h"
+#include "ipcz/node_link.h"
+#include "util/ref_counted.h"
+
+namespace ipcz {
+
+ParcelWrapper::ParcelWrapper(Parcel parcel) : parcel_(std::move(parcel)) {}
+
+ParcelWrapper::~ParcelWrapper() = default;
+
+IpczResult ParcelWrapper::Close() {
+  return IPCZ_RESULT_OK;
+}
+
+IpczResult ParcelWrapper::Reject(uintptr_t context) {
+  const Ref<NodeLink>& remote_source = parcel_.remote_source();
+  if (!remote_source) {
+    return IPCZ_RESULT_FAILED_PRECONDITION;
+  }
+
+  const IpczDriver& driver = remote_source->node()->driver();
+  const Ref<DriverTransport>& transport = remote_source->transport();
+  driver.ReportBadTransportActivity(transport->driver_object().handle(),
+                                    context, IPCZ_NO_FLAGS, nullptr);
+  return IPCZ_RESULT_OK;
+}
+
+IpczResult ParcelWrapper::Get(IpczGetFlags flags,
+                              void* data,
+                              size_t* num_bytes,
+                              IpczHandle* handles,
+                              size_t* num_handles) {
+  if (in_two_phase_get_) {
+    return IPCZ_RESULT_ALREADY_EXISTS;
+  }
+
+  const bool allow_partial = (flags & IPCZ_GET_PARTIAL) != 0;
+  const size_t data_capacity = num_bytes ? *num_bytes : 0;
+  const size_t handles_capacity = num_handles ? *num_handles : 0;
+  if ((data_capacity && !data) || (handles_capacity && !handles)) {
+    return IPCZ_RESULT_INVALID_ARGUMENT;
+  }
+
+  const size_t data_size = allow_partial
+                               ? std::min(parcel_.data_size(), data_capacity)
+                               : parcel_.data_size();
+  const size_t handles_size =
+      allow_partial ? std::min(parcel_.num_objects(), handles_capacity)
+                    : parcel_.num_objects();
+  if (num_bytes) {
+    *num_bytes = data_size;
+  }
+  if (num_handles) {
+    *num_handles = handles_size;
+  }
+
+  const bool consuming_whole_parcel =
+      (data_capacity >= data_size && handles_capacity >= handles_size);
+  if (!consuming_whole_parcel && !allow_partial) {
+    return IPCZ_RESULT_RESOURCE_EXHAUSTED;
+  }
+
+  memcpy(data, parcel_.data_view().data(), data_size);
+  parcel_.Consume(data_size, absl::MakeSpan(handles, handles_size));
+  return IPCZ_RESULT_OK;
+}
+
+IpczResult ParcelWrapper::BeginGet(const void** data,
+                                   size_t* num_data_bytes,
+                                   size_t* num_handles) {
+  if (in_two_phase_get_) {
+    return IPCZ_RESULT_ALREADY_EXISTS;
+  }
+
+  if (data) {
+    *data = parcel_.data_view().data();
+  }
+  if (num_data_bytes) {
+    *num_data_bytes = parcel_.data_size();
+  }
+  if (num_handles) {
+    *num_handles = parcel_.num_objects();
+  }
+  if ((parcel_.data_size() && (!data || !num_data_bytes)) ||
+      (parcel_.num_objects() && !num_handles)) {
+    return IPCZ_RESULT_RESOURCE_EXHAUSTED;
+  }
+
+  in_two_phase_get_ = true;
+  return IPCZ_RESULT_OK;
+}
+
+IpczResult ParcelWrapper::CommitGet(size_t num_data_bytes_consumed,
+                                    absl::Span<IpczHandle> handles) {
+  if (!in_two_phase_get_) {
+    return IPCZ_RESULT_FAILED_PRECONDITION;
+  }
+
+  if (num_data_bytes_consumed > parcel_.data_size() ||
+      handles.size() > parcel_.num_objects()) {
+    return IPCZ_RESULT_OUT_OF_RANGE;
+  }
+
+  parcel_.Consume(num_data_bytes_consumed, handles);
+  in_two_phase_get_ = false;
+  return IPCZ_RESULT_OK;
+}
+
+IpczResult ParcelWrapper::AbortGet() {
+  if (!in_two_phase_get_) {
+    return IPCZ_RESULT_FAILED_PRECONDITION;
+  }
+
+  in_two_phase_get_ = false;
+  return IPCZ_RESULT_OK;
+}
+
+}  // namespace ipcz
diff --git a/src/ipcz/parcel_wrapper.h b/src/ipcz/parcel_wrapper.h
new file mode 100644
index 0000000..843f7d2
--- /dev/null
+++ b/src/ipcz/parcel_wrapper.h
@@ -0,0 +1,56 @@
+// Copyright 2022 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IPCZ_SRC_IPCZ_PARCEL_WRAPPER_
+#define IPCZ_SRC_IPCZ_PARCEL_WRAPPER_
+
+#include <cstddef>
+
+#include "ipcz/api_object.h"
+#include "ipcz/ipcz.h"
+#include "ipcz/parcel.h"
+#include "util/ref_counted.h"
+
+namespace ipcz {
+
+// A parcel wrapper owns a Parcel received from a portal, after the Parcel has
+// been retrieved from the portal by the application.
+//
+// Applications can use these objects to perform two-phase reads of parcels
+// without blocking the receiving portal, and to report their own
+// application-level validation failures to ipcz via the Reject() API.
+class ParcelWrapper : public APIObjectImpl<ParcelWrapper, APIObject::kParcel> {
+ public:
+  explicit ParcelWrapper(Parcel parcel);
+
+  // APIObject:
+  IpczResult Close() override;
+
+  // Signals application-level rejection of this parcel. `context` is an opaque
+  // value passed by the application and propagated to the driver when
+  // appropriate. See the Reject() API.
+  IpczResult Reject(uintptr_t context);
+
+  IpczResult Get(IpczGetFlags flags,
+                 void* data,
+                 size_t* num_data_bytes,
+                 IpczHandle* handles,
+                 size_t* num_handles);
+  IpczResult BeginGet(const void** data,
+                      size_t* num_bytes,
+                      size_t* num_handles);
+  IpczResult CommitGet(size_t num_data_bytes_consumed,
+                       absl::Span<IpczHandle> handles);
+  IpczResult AbortGet();
+
+ private:
+  ~ParcelWrapper() override;
+
+  Parcel parcel_;
+  bool in_two_phase_get_ = false;
+};
+
+}  // namespace ipcz
+
+#endif  // IPCZ_SRC_IPCZ_PARCEL_WRAPPER_
diff --git a/src/ipcz/portal.cc b/src/ipcz/portal.cc
index fd8d645..270b747 100644
--- a/src/ipcz/portal.cc
+++ b/src/ipcz/portal.cc
@@ -210,9 +210,9 @@
                        size_t* num_data_bytes,
                        IpczHandle* handles,
                        size_t* num_handles,
-                       IpczHandle* validator) {
+                       IpczHandle* parcel) {
   return router_->GetNextInboundParcel(flags, data, num_data_bytes, handles,
-                                       num_handles, validator);
+                                       num_handles, parcel);
 }
 
 IpczResult Portal::BeginGet(const void** data,
@@ -236,15 +236,14 @@
 }
 
 IpczResult Portal::CommitGet(size_t num_data_bytes_consumed,
-                             absl::Span<IpczHandle> handles,
-                             IpczHandle* validator) {
+                             absl::Span<IpczHandle> handles) {
   absl::MutexLock lock(&mutex_);
   if (!in_two_phase_get_) {
     return IPCZ_RESULT_FAILED_PRECONDITION;
   }
 
-  IpczResult result = router_->CommitGetNextIncomingParcel(
-      num_data_bytes_consumed, handles, validator);
+  IpczResult result =
+      router_->CommitGetNextIncomingParcel(num_data_bytes_consumed, handles);
   if (result == IPCZ_RESULT_OK) {
     in_two_phase_get_ = false;
   }
diff --git a/src/ipcz/portal.h b/src/ipcz/portal.h
index e646cad..7c09682 100644
--- a/src/ipcz/portal.h
+++ b/src/ipcz/portal.h
@@ -62,13 +62,12 @@
                  size_t* num_data_bytes,
                  IpczHandle* handles,
                  size_t* num_handles,
-                 IpczHandle* validator);
+                 IpczHandle* parcel);
   IpczResult BeginGet(const void** data,
                       size_t* num_data_bytes,
                       size_t* num_handles);
   IpczResult CommitGet(size_t num_data_bytes_consumed,
-                       absl::Span<IpczHandle> handles,
-                       IpczHandle* validator);
+                       absl::Span<IpczHandle> handles);
   IpczResult AbortGet();
 
  private:
diff --git a/src/ipcz/router.cc b/src/ipcz/router.cc
index 582bb21..280f4f2 100644
--- a/src/ipcz/router.cc
+++ b/src/ipcz/router.cc
@@ -14,10 +14,10 @@
 #include "ipcz/local_router_link.h"
 #include "ipcz/node_link.h"
 #include "ipcz/operation_context.h"
+#include "ipcz/parcel_wrapper.h"
 #include "ipcz/remote_router_link.h"
 #include "ipcz/sequence_number.h"
 #include "ipcz/trap_event_dispatcher.h"
-#include "ipcz/validator.h"
 #include "third_party/abseil-cpp/absl/base/macros.h"
 #include "third_party/abseil-cpp/absl/container/inlined_vector.h"
 #include "third_party/abseil-cpp/absl/synchronization/mutex.h"
@@ -444,11 +444,11 @@
                                         size_t* num_bytes,
                                         IpczHandle* handles,
                                         size_t* num_handles,
-                                        IpczHandle* validator) {
+                                        IpczHandle* parcel) {
   const OperationContext context{OperationContext::kAPICall};
   TrapEventDispatcher dispatcher;
   Ref<RouterLink> link_to_notify;
-  Ref<NodeLink> remote_source;
+  Parcel consumed_parcel;
   {
     absl::MutexLock lock(&mutex_);
     if (inbound_parcels_.IsSequenceFullyConsumed()) {
@@ -459,9 +459,14 @@
     }
 
     Parcel& p = inbound_parcels_.NextElement();
+    const bool parcel_only = (flags & IPCZ_GET_PARCEL_ONLY) != 0;
     const bool allow_partial = (flags & IPCZ_GET_PARTIAL) != 0;
     const size_t data_capacity = num_bytes ? *num_bytes : 0;
     const size_t handles_capacity = num_handles ? *num_handles : 0;
+    if ((data_capacity && !data) || (handles_capacity && !handles)) {
+      return IPCZ_RESULT_INVALID_ARGUMENT;
+    }
+
     const size_t data_size =
         allow_partial ? std::min(p.data_size(), data_capacity) : p.data_size();
     const size_t handles_size =
@@ -475,20 +480,29 @@
     }
 
     const bool consuming_whole_parcel =
-        data_capacity >= data_size && handles_capacity >= handles_size;
+        parcel_only ||
+        (data_capacity >= data_size && handles_capacity >= handles_size);
     if (!consuming_whole_parcel && !allow_partial) {
       return IPCZ_RESULT_RESOURCE_EXHAUSTED;
     }
 
-    if (validator) {
-      remote_source = p.remote_source();
+    if (parcel_only) {
+      const bool ok = inbound_parcels_.Pop(consumed_parcel);
+      ABSL_ASSERT(ok);
+    } else {
+      memcpy(data, p.data_view().data(), data_size);
+      if (consuming_whole_parcel) {
+        const bool ok = inbound_parcels_.Pop(consumed_parcel);
+        ABSL_ASSERT(ok);
+        consumed_parcel.Consume(data_size,
+                                absl::MakeSpan(handles, handles_size));
+      } else {
+        const bool ok = inbound_parcels_.Consume(
+            data_size, absl::MakeSpan(handles, handles_size));
+        ABSL_ASSERT(ok);
+      }
     }
 
-    memcpy(data, p.data_view().data(), data_size);
-    const bool ok = inbound_parcels_.Consume(
-        data_size, absl::MakeSpan(handles, handles_size));
-    ABSL_ASSERT(ok);
-
     status_.num_local_parcels = inbound_parcels_.GetNumAvailableElements();
     status_.num_local_bytes = inbound_parcels_.GetTotalAvailableElementSize();
     if (inbound_parcels_.IsSequenceFullyConsumed()) {
@@ -506,9 +520,9 @@
     link_to_notify->SnapshotPeerQueueState(context);
   }
 
-  if (validator) {
-    *validator = Validator::ReleaseAsHandle(
-        MakeRefCounted<Validator>(std::move(remote_source)));
+  if (parcel) {
+    *parcel = ParcelWrapper::ReleaseAsHandle(
+        MakeRefCounted<ParcelWrapper>(std::move(consumed_parcel)));
   }
 
   return IPCZ_RESULT_OK;
@@ -545,11 +559,9 @@
 }
 
 IpczResult Router::CommitGetNextIncomingParcel(size_t num_data_bytes_consumed,
-                                               absl::Span<IpczHandle> handles,
-                                               IpczHandle* validator) {
+                                               absl::Span<IpczHandle> handles) {
   const OperationContext context{OperationContext::kAPICall};
   Ref<RouterLink> link_to_notify;
-  Ref<NodeLink> remote_source;
   TrapEventDispatcher dispatcher;
   {
     absl::MutexLock lock(&mutex_);
@@ -566,10 +578,6 @@
       return IPCZ_RESULT_OUT_OF_RANGE;
     }
 
-    if (validator) {
-      remote_source = p.remote_source();
-    }
-
     const bool ok = inbound_parcels_.Consume(num_data_bytes_consumed, handles);
     ABSL_ASSERT(ok);
 
@@ -590,11 +598,6 @@
     link_to_notify->SnapshotPeerQueueState(context);
   }
 
-  if (validator) {
-    *validator = Validator::ReleaseAsHandle(
-        MakeRefCounted<Validator>(std::move(remote_source)));
-  }
-
   return IPCZ_RESULT_OK;
 }
 
diff --git a/src/ipcz/router.h b/src/ipcz/router.h
index eb43fb5..2663a42 100644
--- a/src/ipcz/router.h
+++ b/src/ipcz/router.h
@@ -158,7 +158,7 @@
                                   size_t* num_bytes,
                                   IpczHandle* handles,
                                   size_t* num_handles,
-                                  IpczHandle* validator);
+                                  IpczHandle* parcel);
 
   // Begins a two-phase retrieval of the next available inbound parcel.
   IpczResult BeginGetNextIncomingParcel(const void** data,
@@ -169,8 +169,7 @@
   // consuming some (possibly all) bytes and handles from that parcel. Once a
   // parcel is fully consumed, it's removed from the inbound queue.
   IpczResult CommitGetNextIncomingParcel(size_t num_data_bytes_consumed,
-                                         absl::Span<IpczHandle> handles,
-                                         IpczHandle* validator);
+                                         absl::Span<IpczHandle> handles);
 
   // Attempts to install a new trap on this Router, to invoke `handler` as soon
   // as one or more conditions in `conditions` is met. This method effectively
diff --git a/src/ipcz/validator.cc b/src/ipcz/validator.cc
deleted file mode 100644
index bba6ad3..0000000
--- a/src/ipcz/validator.cc
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2022 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "ipcz/validator.h"
-
-#include "ipcz/driver_object.h"
-#include "ipcz/driver_transport.h"
-#include "ipcz/ipcz.h"
-#include "ipcz/node.h"
-#include "ipcz/node_link.h"
-#include "util/ref_counted.h"
-
-namespace ipcz {
-
-Validator::Validator(Ref<NodeLink> remote_source)
-    : remote_source_(std::move(remote_source)) {}
-
-Validator::~Validator() = default;
-
-IpczResult Validator::Close() {
-  return IPCZ_RESULT_OK;
-}
-
-IpczResult Validator::Reject(uintptr_t context) {
-  if (!remote_source_) {
-    return IPCZ_RESULT_FAILED_PRECONDITION;
-  }
-
-  const IpczDriver& driver = remote_source_->node()->driver();
-  const Ref<DriverTransport>& transport = remote_source_->transport();
-  driver.ReportBadTransportActivity(transport->driver_object().handle(),
-                                    context, IPCZ_NO_FLAGS, nullptr);
-  return IPCZ_RESULT_OK;
-}
-
-}  // namespace ipcz
diff --git a/src/ipcz/validator.h b/src/ipcz/validator.h
deleted file mode 100644
index 556f25d..0000000
--- a/src/ipcz/validator.h
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2022 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef IPCZ_SRC_IPCZ_VALIDATOR_H_
-#define IPCZ_SRC_IPCZ_VALIDATOR_H_
-
-#include "ipcz/validator.h"
-
-#include "ipcz/api_object.h"
-#include "util/ref_counted.h"
-
-namespace ipcz {
-
-class NodeLink;
-
-// A validator object retains context associated with a specific inbound parcel.
-// Applications can use these objects to report their own application-level
-// validation failures to ipcz, and ipcz can use the context within to propagate
-// the failure out to an appropriate driver transport.
-class Validator : public APIObjectImpl<Validator, APIObject::kValidator> {
- public:
-  explicit Validator(Ref<NodeLink> remote_source);
-
-  // APIObject:
-  IpczResult Close() override;
-
-  // Signals application-level rejection of whatever this validator is
-  // associated with. `context` is an opaque value passed by the application
-  // and propagated to the driver when appropriate. See the Reject() API.
-  IpczResult Reject(uintptr_t context);
-
- private:
-  ~Validator() override;
-
-  // The remote source which sent the parcel to the local node. If this is null,
-  // the parcel originated from the local node.
-  const Ref<NodeLink> remote_source_;
-};
-
-}  // namespace ipcz
-
-#endif  // IPCZ_SRC_IPCZ_VALIDATOR_H_
diff --git a/src/parcel_test.cc b/src/parcel_test.cc
new file mode 100644
index 0000000..c755b03
--- /dev/null
+++ b/src/parcel_test.cc
@@ -0,0 +1,214 @@
+// Copyright 2022 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <cstring>
+#include <string_view>
+
+#include "ipcz/ipcz.h"
+#include "test/multinode_test.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "util/ref_counted.h"
+
+namespace ipcz {
+namespace {
+
+class ParcelTestNode : public test::TestNode {
+ public:
+  IpczResult WaitForParcel(IpczHandle portal) {
+    const IpczTrapConditions conditions = {
+        .size = sizeof(conditions),
+        .flags = IPCZ_TRAP_ABOVE_MIN_LOCAL_PARCELS,
+        .min_local_parcels = 0,
+    };
+    return WaitForConditions(portal, conditions);
+  }
+};
+
+using ParcelTest = test::MultinodeTest<ParcelTestNode>;
+
+constexpr std::string_view kMessage = "here's that box of hornets you wanted";
+constexpr std::string_view kHornets = "bzzzzz";
+
+MULTINODE_TEST_NODE(ParcelTestNode, GetClient) {
+  IpczHandle b = ConnectToBroker();
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForParcel(b));
+
+  // Retrieving a parcel object removes it from its portal's queue.
+  IpczHandle parcel = 0;
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().Get(b, IPCZ_GET_PARCEL_ONLY, nullptr, nullptr, nullptr,
+                       nullptr, nullptr, &parcel));
+  EXPECT_EQ(IPCZ_RESULT_UNAVAILABLE,
+            ipcz().Get(b, IPCZ_GET_PARCEL_ONLY, nullptr, nullptr, nullptr,
+                       nullptr, nullptr, &parcel));
+
+  // Short reads behave as with portals, providing parcel dimensions on output.
+  size_t num_bytes = 0;
+  size_t num_handles = 0;
+  EXPECT_EQ(IPCZ_RESULT_RESOURCE_EXHAUSTED,
+            ipcz().Get(parcel, IPCZ_NO_FLAGS, nullptr, nullptr, &num_bytes,
+                       nullptr, &num_handles, nullptr));
+  EXPECT_EQ(kMessage.size(), num_bytes);
+  EXPECT_EQ(1u, num_handles);
+
+  // Invalid arguments: null data or handles with non-zero capacity.
+  EXPECT_EQ(IPCZ_RESULT_INVALID_ARGUMENT,
+            ipcz().Get(parcel, IPCZ_NO_FLAGS, nullptr, nullptr, &num_bytes,
+                       nullptr, nullptr, nullptr));
+  EXPECT_EQ(IPCZ_RESULT_INVALID_ARGUMENT,
+            ipcz().Get(parcel, IPCZ_NO_FLAGS, nullptr, nullptr, nullptr,
+                       nullptr, &num_handles, nullptr));
+
+  // Verify the contents.
+  char buffer[kMessage.size()];
+  IpczHandle box;
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().Get(parcel, IPCZ_NO_FLAGS, nullptr, buffer, &num_bytes, &box,
+                       &num_handles, nullptr));
+  EXPECT_EQ(kMessage.size(), num_bytes);
+  EXPECT_EQ(1u, num_handles);
+  EXPECT_EQ(kMessage, std::string_view(buffer, num_bytes));
+
+  // Contents of the parcel are consumed by Get(), so now there's nothing left.
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().Get(parcel, IPCZ_NO_FLAGS, nullptr, buffer, &num_bytes, &box,
+                       &num_handles, nullptr));
+  EXPECT_EQ(0u, num_bytes);
+  EXPECT_EQ(0u, num_handles);
+
+  // Send the contents of the box back as another parcel.
+  Put(b, UnboxBlob(box));
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(b, IPCZ_TRAP_DEAD));
+  CloseAll({b, parcel});
+}
+
+MULTINODE_TEST(ParcelTest, Get) {
+  IpczHandle c = SpawnTestNode<GetClient>();
+
+  IpczHandle blob = BoxBlob(kHornets);
+  Put(c, kMessage, {&blob, 1});
+
+  std::string message;
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(c, &message));
+  EXPECT_EQ(kHornets, message);
+  Close(c);
+}
+
+MULTINODE_TEST_NODE(ParcelTestNode, TwoPhaseGetClient) {
+  IpczHandle b = ConnectToBroker();
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForParcel(b));
+
+  IpczHandle parcel = 0;
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().Get(b, IPCZ_GET_PARCEL_ONLY, nullptr, nullptr, nullptr,
+                       nullptr, nullptr, &parcel));
+
+  // Various combinations of missing args return to indicate size requirements.
+  const void* data;
+  size_t num_bytes;
+  size_t num_handles;
+  EXPECT_EQ(IPCZ_RESULT_RESOURCE_EXHAUSTED,
+            ipcz().BeginGet(parcel, IPCZ_NO_FLAGS, nullptr, nullptr, nullptr,
+                            nullptr));
+  EXPECT_EQ(IPCZ_RESULT_RESOURCE_EXHAUSTED,
+            ipcz().BeginGet(parcel, IPCZ_NO_FLAGS, nullptr, &data, &num_bytes,
+                            nullptr));
+  EXPECT_EQ(IPCZ_RESULT_RESOURCE_EXHAUSTED,
+            ipcz().BeginGet(parcel, IPCZ_NO_FLAGS, nullptr, nullptr, &num_bytes,
+                            &num_handles));
+  EXPECT_EQ(IPCZ_RESULT_RESOURCE_EXHAUSTED,
+            ipcz().BeginGet(parcel, IPCZ_NO_FLAGS, nullptr, nullptr, nullptr,
+                            &num_handles));
+  EXPECT_EQ(IPCZ_RESULT_OK, ipcz().BeginGet(parcel, IPCZ_NO_FLAGS, nullptr,
+                                            &data, &num_bytes, &num_handles));
+  EXPECT_EQ(kMessage.size(), num_bytes);
+  EXPECT_EQ(1u, num_handles);
+
+  // We can't start a new get of any kind during the two-phase get.
+  EXPECT_EQ(IPCZ_RESULT_ALREADY_EXISTS,
+            ipcz().BeginGet(parcel, IPCZ_NO_FLAGS, nullptr, &data, &num_bytes,
+                            &num_handles));
+  EXPECT_EQ(IPCZ_RESULT_ALREADY_EXISTS,
+            ipcz().Get(parcel, IPCZ_NO_FLAGS, nullptr, &data, &num_bytes,
+                       nullptr, &num_handles, nullptr));
+
+  // Two-phase gets on parcels can be aborted.
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().EndGet(parcel, 0, 0, IPCZ_END_GET_ABORT, nullptr, nullptr));
+  EXPECT_EQ(IPCZ_RESULT_OK, ipcz().BeginGet(parcel, IPCZ_NO_FLAGS, nullptr,
+                                            &data, &num_bytes, &num_handles));
+  EXPECT_EQ(kMessage.size(), num_bytes);
+  EXPECT_EQ(1u, num_handles);
+
+  // Verify the contents, and partially consume the parcel.
+  IpczHandle box;
+  EXPECT_EQ(kMessage,
+            std::string_view(static_cast<const char*>(data), num_bytes));
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().EndGet(parcel, 1, 1, IPCZ_NO_FLAGS, nullptr, &box));
+
+  // A new two-phase read should see the first byte gone as well as the box.
+  EXPECT_EQ(IPCZ_RESULT_OK, ipcz().BeginGet(parcel, IPCZ_NO_FLAGS, nullptr,
+                                            &data, &num_bytes, &num_handles));
+  EXPECT_EQ(kMessage.size() - 1, num_bytes);
+  EXPECT_EQ(kMessage.substr(1),
+            std::string_view(static_cast<const char*>(data), num_bytes));
+  EXPECT_EQ(0u, num_handles);
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().EndGet(parcel, 0, 0, IPCZ_END_GET_ABORT, nullptr, nullptr));
+
+  // Send the contents of the box back as another parcel.
+  Put(b, UnboxBlob(box));
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(b, IPCZ_TRAP_DEAD));
+  CloseAll({b, parcel});
+}
+
+MULTINODE_TEST(ParcelTest, TwoPhaseGet) {
+  IpczHandle c = SpawnTestNode<TwoPhaseGetClient>();
+
+  IpczHandle blob = BoxBlob(kHornets);
+  Put(c, kMessage, {&blob, 1});
+
+  std::string message;
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(c, &message));
+  EXPECT_EQ(kHornets, message);
+  Close(c);
+}
+
+MULTINODE_TEST_NODE(ParcelTestNode, CloseClient) {
+  IpczHandle b = ConnectToBroker();
+  IpczHandle parcel;
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForParcel(b));
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().Get(b, IPCZ_GET_PARCEL_ONLY, nullptr, nullptr, nullptr,
+                       nullptr, nullptr, &parcel));
+
+  size_t num_bytes = 0;
+  size_t num_handles = 0;
+  EXPECT_EQ(IPCZ_RESULT_RESOURCE_EXHAUSTED,
+            ipcz().Get(parcel, IPCZ_NO_FLAGS, nullptr, nullptr, &num_bytes,
+                       nullptr, &num_handles, nullptr));
+  EXPECT_EQ(kMessage.size(), num_bytes);
+  EXPECT_EQ(1u, num_handles);
+
+  // Closing the parcel should close any attached objects. The broker should
+  // observer its `q` portal dying, because that portal's peer was attached to
+  // this parcel.
+  Close(parcel);
+
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(b, IPCZ_TRAP_DEAD));
+  Close(b);
+}
+
+MULTINODE_TEST(ParcelTest, Close) {
+  IpczHandle c = SpawnTestNode<CloseClient>();
+  auto [q, p] = OpenPortals();
+  Put(c, kMessage, {&p, 1});
+
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(q, IPCZ_TRAP_DEAD));
+  CloseAll({c, q});
+}
+
+}  // namespace
+}  // namespace ipcz
diff --git a/src/queueing_test.cc b/src/queueing_test.cc
index 96fe472..635ccec 100644
--- a/src/queueing_test.cc
+++ b/src/queueing_test.cc
@@ -123,8 +123,8 @@
   // The producer should only have been able to put 3 out of its 4 bytes.
   EXPECT_EQ("ipc",
             std::string_view(reinterpret_cast<const char*>(data), num_bytes));
-  EXPECT_EQ(IPCZ_RESULT_OK, ipcz().EndGet(b, num_bytes, 0, IPCZ_NO_FLAGS,
-                                          nullptr, nullptr, nullptr));
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().EndGet(b, num_bytes, 0, IPCZ_NO_FLAGS, nullptr, nullptr));
 
   Close(b);
 }
@@ -174,8 +174,8 @@
 
   EXPECT_EQ("hello?",
             std::string_view(reinterpret_cast<const char*>(data), num_bytes));
-  EXPECT_EQ(IPCZ_RESULT_OK, ipcz().EndGet(b, num_bytes, 0, IPCZ_NO_FLAGS,
-                                          nullptr, nullptr, nullptr));
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().EndGet(b, num_bytes, 0, IPCZ_NO_FLAGS, nullptr, nullptr));
   Close(b);
 }
 
@@ -215,7 +215,7 @@
       EXPECT_EQ(std::string_view(static_cast<const char*>(data), num_bytes),
                 std::string(num_bytes, '!'));
       EXPECT_EQ(IPCZ_RESULT_OK, ipcz().EndGet(b, num_bytes, 0, IPCZ_NO_FLAGS,
-                                              nullptr, nullptr, nullptr));
+                                              nullptr, nullptr));
       continue;
     }