mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-08 10:56:13 -08:00
The core raised DATA_PLANE_ABI_VERSION to 4 in f26c2aa1 ("feat(wasi):
run EasyTier core on Cloudflare Workers and browsers") to advertise the
new guest exports easytier_data_plane_tcp_shutdown_write_submit and
easytier_data_plane_tcp_shutdown_write_result_take, which let a host
half-close guest TCP streams.
The Go host has no caller for half-close: net.Conn exposes only Close,
which already tears down both directions, so the v3 behavior is
preserved and no new plumbing is added. Without this bump, any artifact
rebuilt from current core source is rejected at instance creation with
"unsupported EasyTier data plane ABI version 4, want 3".
716 lines
16 KiB
Go
716 lines
16 KiB
Go
package coreabi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"net/netip"
|
|
|
|
"github.com/easytier/easytier/easytier-go/internal/contextutil"
|
|
)
|
|
|
|
const (
|
|
DataPlaneABIVersion = 4
|
|
|
|
DataPlaneCapability uint64 = 1 << 0
|
|
DataPlaneTCPCapability uint64 = 1 << 1
|
|
DataPlaneUDPCapability uint64 = 1 << 2
|
|
|
|
DeadlineRead DeadlineDirection = 1 << 0
|
|
DeadlineWrite DeadlineDirection = 1 << 1
|
|
|
|
requiredDataPlaneCapabilities = DataPlaneCapability |
|
|
DataPlaneTCPCapability |
|
|
DataPlaneUDPCapability
|
|
)
|
|
|
|
type OperationID uint64
|
|
type ResourceID uint64
|
|
type DeadlineDirection uint32
|
|
|
|
type OperationKind uint16
|
|
|
|
const (
|
|
OperationTCPConnect OperationKind = iota + 1
|
|
OperationTCPBind
|
|
OperationTCPAccept
|
|
OperationTCPRead
|
|
OperationTCPWrite
|
|
OperationUDPBind
|
|
OperationUDPReceive
|
|
OperationUDPSend
|
|
)
|
|
|
|
func (kind OperationKind) valid() bool {
|
|
return kind >= OperationTCPConnect && kind <= OperationUDPSend
|
|
}
|
|
|
|
type ErrorKind uint16
|
|
|
|
const (
|
|
ErrorNone ErrorKind = iota
|
|
ErrorCancelled
|
|
ErrorDeadlineExceeded
|
|
ErrorInstanceStopped
|
|
ErrorHandleClosed
|
|
ErrorNoOverlayRoute
|
|
ErrorPathNotReady
|
|
ErrorAddressFamilyUnsupported
|
|
ErrorAddressInUse
|
|
ErrorConnectionRefused
|
|
ErrorNetworkChanged
|
|
ErrorResourceLimit
|
|
ErrorIO
|
|
ErrorBufferTooSmall
|
|
)
|
|
|
|
func (kind ErrorKind) validCompletionStatus() bool {
|
|
return kind <= ErrorBufferTooSmall
|
|
}
|
|
|
|
type DataPlaneError struct {
|
|
Kind ErrorKind
|
|
Message string
|
|
}
|
|
|
|
func (err *DataPlaneError) Error() string {
|
|
if err.Message == "" {
|
|
return fmt.Sprintf("data plane error %d", err.Kind)
|
|
}
|
|
return err.Message
|
|
}
|
|
|
|
type Completion struct {
|
|
Operation OperationID
|
|
Kind OperationKind
|
|
Status ErrorKind
|
|
}
|
|
|
|
type OperationResult struct {
|
|
Kind OperationKind
|
|
Resource ResourceID
|
|
Local netip.AddrPort
|
|
Peer netip.AddrPort
|
|
Data []byte
|
|
Length int
|
|
EOF bool
|
|
Truncated bool
|
|
}
|
|
|
|
func (core *Core) validateDataPlaneABI(ctx context.Context) error {
|
|
version, err := core.callOne(ctx, "easytier_data_plane_abi_version")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if version != DataPlaneABIVersion {
|
|
return fmt.Errorf(
|
|
"unsupported EasyTier data plane ABI version %d, want %d",
|
|
version,
|
|
DataPlaneABIVersion,
|
|
)
|
|
}
|
|
capabilities, err := core.callOne(ctx, "easytier_data_plane_capabilities")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if missing := requiredDataPlaneCapabilities &^ capabilities; missing != 0 {
|
|
return fmt.Errorf(
|
|
"EasyTier data plane capabilities %#x are missing %#x",
|
|
capabilities,
|
|
missing,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (core *Core) SubmitTCPConnect(
|
|
ctx context.Context,
|
|
peer netip.AddrPort,
|
|
timeoutMillis uint64,
|
|
) (OperationID, error) {
|
|
wire, err := encodeSocketAddress(peer)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return core.submitWithInput(
|
|
ctx,
|
|
"easytier_data_plane_tcp_connect_submit",
|
|
wire[:],
|
|
timeoutMillis,
|
|
)
|
|
}
|
|
|
|
func (core *Core) SubmitTCPBind(
|
|
ctx context.Context,
|
|
port uint16,
|
|
timeoutMillis uint64,
|
|
) (OperationID, error) {
|
|
return core.submit(
|
|
ctx,
|
|
"easytier_data_plane_tcp_bind_submit",
|
|
uint64(port),
|
|
timeoutMillis,
|
|
)
|
|
}
|
|
|
|
func (core *Core) SubmitTCPAccept(
|
|
ctx context.Context,
|
|
listener ResourceID,
|
|
timeoutMillis uint64,
|
|
) (OperationID, error) {
|
|
return core.submit(
|
|
ctx,
|
|
"easytier_data_plane_tcp_accept_submit",
|
|
uint64(listener),
|
|
timeoutMillis,
|
|
)
|
|
}
|
|
|
|
func (core *Core) SubmitTCPRead(
|
|
ctx context.Context,
|
|
stream ResourceID,
|
|
maximum uint32,
|
|
) (OperationID, error) {
|
|
return core.submit(
|
|
ctx,
|
|
"easytier_data_plane_tcp_read_submit",
|
|
uint64(stream),
|
|
uint64(maximum),
|
|
)
|
|
}
|
|
|
|
func (core *Core) SubmitTCPWrite(
|
|
ctx context.Context,
|
|
stream ResourceID,
|
|
data []byte,
|
|
) (operation OperationID, err error) {
|
|
if len(data) > maxDataPlaneTransferBytes {
|
|
return 0, fmt.Errorf(
|
|
"TCP write length %d exceeds data plane limit %d",
|
|
len(data),
|
|
maxDataPlaneTransferBytes,
|
|
)
|
|
}
|
|
pointer, err := core.writeInput(ctx, data)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if pointer != 0 {
|
|
defer core.cleanupBuffer(ctx, pointer, &err)
|
|
}
|
|
return core.submit(
|
|
ctx,
|
|
"easytier_data_plane_tcp_write_submit",
|
|
uint64(stream),
|
|
uint64(pointer),
|
|
uint64(len(data)),
|
|
)
|
|
}
|
|
|
|
func (core *Core) SubmitUDPBind(
|
|
ctx context.Context,
|
|
port uint16,
|
|
timeoutMillis uint64,
|
|
) (OperationID, error) {
|
|
return core.submit(
|
|
ctx,
|
|
"easytier_data_plane_udp_bind_submit",
|
|
uint64(port),
|
|
timeoutMillis,
|
|
)
|
|
}
|
|
|
|
func (core *Core) SubmitUDPReceive(
|
|
ctx context.Context,
|
|
socket ResourceID,
|
|
maximum uint32,
|
|
) (OperationID, error) {
|
|
return core.submit(
|
|
ctx,
|
|
"easytier_data_plane_udp_receive_submit",
|
|
uint64(socket),
|
|
uint64(maximum),
|
|
)
|
|
}
|
|
|
|
func (core *Core) SubmitUDPSend(
|
|
ctx context.Context,
|
|
socket ResourceID,
|
|
peer netip.AddrPort,
|
|
data []byte,
|
|
) (operation OperationID, err error) {
|
|
if len(data) > maxDataPlaneTransferBytes {
|
|
return 0, fmt.Errorf(
|
|
"UDP send length %d exceeds data plane limit %d",
|
|
len(data),
|
|
maxDataPlaneTransferBytes,
|
|
)
|
|
}
|
|
address, err := encodeSocketAddress(peer)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
addressPointer, err := core.ensureDataPlaneAddress(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if !core.module.Memory().Write(addressPointer, address[:]) {
|
|
return 0, fmt.Errorf("write UDP peer address to guest memory")
|
|
}
|
|
var dataPointer uint32
|
|
if len(data) != 0 {
|
|
dataPointer, err = core.ensureDataPlaneInput(ctx, uint32(len(data)))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if !core.module.Memory().Write(dataPointer, data) {
|
|
return 0, fmt.Errorf("write UDP payload to guest memory")
|
|
}
|
|
}
|
|
return core.submit(
|
|
ctx,
|
|
"easytier_data_plane_udp_send_submit",
|
|
uint64(socket),
|
|
uint64(addressPointer),
|
|
uint64(dataPointer),
|
|
uint64(len(data)),
|
|
)
|
|
}
|
|
|
|
func (core *Core) SetResourceDeadline(
|
|
ctx context.Context,
|
|
resource ResourceID,
|
|
direction DeadlineDirection,
|
|
timeoutMillis uint64,
|
|
) error {
|
|
result, err := core.callOne(
|
|
ctx,
|
|
"easytier_data_plane_resource_deadline_set",
|
|
core.handle,
|
|
uint64(resource),
|
|
uint64(direction),
|
|
timeoutMillis,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if status := int32(result); status != 0 {
|
|
return core.dataPlaneStatusError(
|
|
ctx,
|
|
"set data plane resource deadline",
|
|
status,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (core *Core) submitWithInput(
|
|
ctx context.Context,
|
|
name string,
|
|
input []byte,
|
|
params ...uint64,
|
|
) (operation OperationID, err error) {
|
|
pointer, err := core.writeInput(ctx, input)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer core.cleanupBuffer(ctx, pointer, &err)
|
|
return core.submit(ctx, name, append([]uint64{uint64(pointer)}, params...)...)
|
|
}
|
|
|
|
func (core *Core) submit(
|
|
ctx context.Context,
|
|
name string,
|
|
params ...uint64,
|
|
) (operation OperationID, err error) {
|
|
pointer, err := core.ensureDataPlaneOutput(ctx, operationIDLength)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
callParams := make([]uint64, 0, len(params)+2)
|
|
callParams = append(callParams, core.handle)
|
|
callParams = append(callParams, params...)
|
|
callParams = append(callParams, uint64(pointer))
|
|
result, err := core.callOne(ctx, name, callParams...)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if status := int32(result); status != 0 {
|
|
return 0, core.dataPlaneStatusError(ctx, name, status)
|
|
}
|
|
wire, ok := core.module.Memory().Read(pointer, operationIDLength)
|
|
if !ok {
|
|
return 0, fmt.Errorf("read operation ID from guest memory")
|
|
}
|
|
operation = OperationID(binary.BigEndian.Uint64(wire))
|
|
if operation == 0 {
|
|
return 0, fmt.Errorf("%s returned zero operation ID", name)
|
|
}
|
|
return operation, nil
|
|
}
|
|
|
|
func (core *Core) DrainCompletions(
|
|
ctx context.Context,
|
|
maximum uint32,
|
|
) (completions []Completion, err error) {
|
|
if maximum == 0 {
|
|
return nil, nil
|
|
}
|
|
if uint64(maximum)*completionLength > math.MaxUint32 {
|
|
return nil, fmt.Errorf("completion batch %d exceeds guest address space", maximum)
|
|
}
|
|
length := maximum * completionLength
|
|
pointer, err := core.ensureDataPlaneOutput(ctx, length)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result, err := core.callCached(
|
|
ctx,
|
|
"easytier_data_plane_completion_drain",
|
|
&core.completionFunction,
|
|
core.handle,
|
|
uint64(pointer),
|
|
uint64(maximum),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
count := int32(result)
|
|
if count < 0 {
|
|
return nil, core.dataPlaneStatusError(
|
|
ctx,
|
|
"drain data plane completions",
|
|
count,
|
|
)
|
|
}
|
|
if uint32(count) > maximum {
|
|
return nil, fmt.Errorf(
|
|
"data plane drained %d completions into capacity %d",
|
|
count,
|
|
maximum,
|
|
)
|
|
}
|
|
wire, ok := core.module.Memory().Read(
|
|
pointer,
|
|
uint32(count)*completionLength,
|
|
)
|
|
if !ok {
|
|
return nil, fmt.Errorf("read data plane completions from guest memory")
|
|
}
|
|
return decodeCompletions(wire)
|
|
}
|
|
|
|
func (core *Core) TakeResult(
|
|
ctx context.Context,
|
|
completion Completion,
|
|
) (OperationResult, error) {
|
|
result := OperationResult{Kind: completion.Kind}
|
|
var err error
|
|
switch completion.Kind {
|
|
case OperationTCPConnect, OperationTCPAccept:
|
|
result.Resource, result.Local, result.Peer, err =
|
|
core.takeStreamResult(ctx, completion)
|
|
case OperationTCPBind, OperationUDPBind:
|
|
result.Resource, result.Local, err =
|
|
core.takeBindResult(ctx, completion)
|
|
case OperationTCPRead:
|
|
result.Data, result.EOF, err = core.takeTCPReadResult(ctx, completion)
|
|
case OperationTCPWrite:
|
|
result.Length, err = core.takeLengthResult(
|
|
ctx,
|
|
completion,
|
|
"easytier_data_plane_tcp_write_result_take",
|
|
)
|
|
case OperationUDPReceive:
|
|
result.Data, result.Peer, result.Truncated, err =
|
|
core.takeUDPReceiveResult(ctx, completion)
|
|
case OperationUDPSend:
|
|
result.Length, err = core.takeLengthResult(
|
|
ctx,
|
|
completion,
|
|
"easytier_data_plane_udp_send_result_take",
|
|
)
|
|
default:
|
|
return OperationResult{}, fmt.Errorf(
|
|
"take unknown data plane operation kind %d",
|
|
completion.Kind,
|
|
)
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
func (core *Core) takeStreamResult(
|
|
ctx context.Context,
|
|
completion Completion,
|
|
) (resource ResourceID, local, peer netip.AddrPort, err error) {
|
|
name := "easytier_data_plane_tcp_connect_result_take"
|
|
if completion.Kind == OperationTCPAccept {
|
|
name = "easytier_data_plane_tcp_accept_result_take"
|
|
}
|
|
wire, err := core.takeFixedResult(ctx, completion, name, streamResultLength)
|
|
if err != nil {
|
|
return 0, netip.AddrPort{}, netip.AddrPort{}, err
|
|
}
|
|
return decodeStreamResult(wire)
|
|
}
|
|
|
|
func (core *Core) takeBindResult(
|
|
ctx context.Context,
|
|
completion Completion,
|
|
) (resource ResourceID, local netip.AddrPort, err error) {
|
|
name := "easytier_data_plane_tcp_bind_result_take"
|
|
if completion.Kind == OperationUDPBind {
|
|
name = "easytier_data_plane_udp_bind_result_take"
|
|
}
|
|
wire, err := core.takeFixedResult(ctx, completion, name, bindResultLength)
|
|
if err != nil {
|
|
return 0, netip.AddrPort{}, err
|
|
}
|
|
return decodeBindResult(wire)
|
|
}
|
|
|
|
func (core *Core) takeFixedResult(
|
|
ctx context.Context,
|
|
completion Completion,
|
|
name string,
|
|
length uint32,
|
|
) (wire []byte, err error) {
|
|
pointer, err := core.ensureDataPlaneOutput(ctx, length)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result, err := core.callOne(
|
|
ctx,
|
|
name,
|
|
core.handle,
|
|
uint64(completion.Operation),
|
|
uint64(pointer),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if status := int32(result); status != 0 {
|
|
return nil, core.dataPlaneStatusError(ctx, name, status)
|
|
}
|
|
bytes, ok := core.module.Memory().Read(pointer, length)
|
|
if !ok {
|
|
return nil, fmt.Errorf("read %s from guest memory", name)
|
|
}
|
|
return append([]byte(nil), bytes...), nil
|
|
}
|
|
|
|
func (core *Core) takeTCPReadResult(
|
|
ctx context.Context,
|
|
completion Completion,
|
|
) (data []byte, eof bool, err error) {
|
|
data, metadata, err := core.takeVariableResult(
|
|
ctx,
|
|
completion,
|
|
"easytier_data_plane_tcp_read_result_take",
|
|
tcpReadMetadataLength,
|
|
)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if metadata[0] > 1 {
|
|
return nil, false, fmt.Errorf(
|
|
"data plane returned invalid TCP EOF flag %d",
|
|
metadata[0],
|
|
)
|
|
}
|
|
return data, metadata[0] == 1, nil
|
|
}
|
|
|
|
func (core *Core) takeUDPReceiveResult(
|
|
ctx context.Context,
|
|
completion Completion,
|
|
) (data []byte, peer netip.AddrPort, truncated bool, err error) {
|
|
data, metadata, err := core.takeVariableResult(
|
|
ctx,
|
|
completion,
|
|
"easytier_data_plane_udp_receive_result_take",
|
|
udpReceiveMetadataLength,
|
|
)
|
|
if err != nil {
|
|
return nil, netip.AddrPort{}, false, err
|
|
}
|
|
peer, err = decodeSocketAddress(metadata[:socketAddressLength])
|
|
if err != nil {
|
|
return nil, netip.AddrPort{}, false, err
|
|
}
|
|
if metadata[socketAddressLength] > 1 {
|
|
return nil, netip.AddrPort{}, false, fmt.Errorf(
|
|
"data plane returned invalid UDP truncation flag %d",
|
|
metadata[socketAddressLength],
|
|
)
|
|
}
|
|
return data, peer, metadata[socketAddressLength] == 1, nil
|
|
}
|
|
|
|
func (core *Core) takeVariableResult(
|
|
ctx context.Context,
|
|
completion Completion,
|
|
name string,
|
|
metadataLength uint32,
|
|
) (data, metadata []byte, err error) {
|
|
capacity := uint32(maxDataPlaneTransferBytes)
|
|
if completion.Kind == OperationUDPReceive {
|
|
capacity = math.MaxUint16
|
|
}
|
|
dataPointer, err := core.ensureDataPlaneInput(ctx, capacity)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
metadataPointer, err := core.ensureDataPlaneOutput(ctx, metadataLength)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
result, err := core.callOne(
|
|
ctx,
|
|
name,
|
|
core.handle,
|
|
uint64(completion.Operation),
|
|
uint64(dataPointer),
|
|
uint64(capacity),
|
|
uint64(metadataPointer),
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
length := int32(result)
|
|
if length < 0 {
|
|
return nil, nil, core.dataPlaneStatusError(ctx, name, length)
|
|
}
|
|
if uint32(length) > capacity {
|
|
return nil, nil, fmt.Errorf(
|
|
"%s returned length %d into capacity %d",
|
|
name,
|
|
length,
|
|
capacity,
|
|
)
|
|
}
|
|
if length != 0 {
|
|
bytes, ok := core.module.Memory().Read(dataPointer, uint32(length))
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("read %s payload from guest memory", name)
|
|
}
|
|
data = append([]byte(nil), bytes...)
|
|
}
|
|
bytes, ok := core.module.Memory().Read(metadataPointer, metadataLength)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("read %s metadata from guest memory", name)
|
|
}
|
|
return data, append([]byte(nil), bytes...), nil
|
|
}
|
|
|
|
func (core *Core) takeLengthResult(
|
|
ctx context.Context,
|
|
completion Completion,
|
|
name string,
|
|
) (int, error) {
|
|
result, err := core.callOne(
|
|
ctx,
|
|
name,
|
|
core.handle,
|
|
uint64(completion.Operation),
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
length := int32(result)
|
|
if length < 0 {
|
|
return 0, core.dataPlaneStatusError(ctx, name, length)
|
|
}
|
|
return int(length), nil
|
|
}
|
|
|
|
func (core *Core) CancelOperation(
|
|
ctx context.Context,
|
|
operation OperationID,
|
|
) error {
|
|
return core.dataPlaneCallStatus(
|
|
ctx,
|
|
"easytier_data_plane_operation_cancel",
|
|
uint64(operation),
|
|
)
|
|
}
|
|
|
|
func (core *Core) FreeOperation(
|
|
ctx context.Context,
|
|
operation OperationID,
|
|
) error {
|
|
return core.dataPlaneCallStatus(
|
|
ctx,
|
|
"easytier_data_plane_operation_free",
|
|
uint64(operation),
|
|
)
|
|
}
|
|
|
|
func (core *Core) CloseResource(ctx context.Context, resource ResourceID) error {
|
|
return core.dataPlaneCallStatus(
|
|
ctx,
|
|
"easytier_data_plane_resource_close",
|
|
uint64(resource),
|
|
)
|
|
}
|
|
|
|
func (core *Core) dataPlaneCallStatus(
|
|
ctx context.Context,
|
|
name string,
|
|
params ...uint64,
|
|
) error {
|
|
callParams := append([]uint64{core.handle}, params...)
|
|
result, err := core.callOne(ctx, name, callParams...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if status := int32(result); status != 0 {
|
|
return core.dataPlaneStatusError(ctx, name, status)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (core *Core) dataPlaneStatusError(
|
|
ctx context.Context,
|
|
operation string,
|
|
status int32,
|
|
) error {
|
|
if status >= 0 || status < -int32(ErrorBufferTooSmall) {
|
|
return core.statusError(ctx, operation, status)
|
|
}
|
|
message, err := core.errorMessage(ctx, core.handle)
|
|
if err != nil {
|
|
return errors.Join(
|
|
&DataPlaneError{Kind: ErrorKind(-status)},
|
|
fmt.Errorf("%s: %w", operation, err),
|
|
)
|
|
}
|
|
return &DataPlaneError{
|
|
Kind: ErrorKind(-status),
|
|
Message: message,
|
|
}
|
|
}
|
|
|
|
func (core *Core) writeInput(
|
|
ctx context.Context,
|
|
data []byte,
|
|
) (uint32, error) {
|
|
if len(data) == 0 {
|
|
return 0, nil
|
|
}
|
|
if uint64(len(data)) > math.MaxUint32 {
|
|
return 0, fmt.Errorf("guest input length %d exceeds wasm32", len(data))
|
|
}
|
|
pointer, err := core.allocate(ctx, uint32(len(data)))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if !core.module.Memory().Write(pointer, data) {
|
|
_ = core.free(contextutil.WithoutCancel(ctx), pointer)
|
|
return 0, fmt.Errorf("write data plane input to guest memory")
|
|
}
|
|
return pointer, nil
|
|
}
|