snapshot: sagernet/gvisor v0.0.0-20260727.0-sing-box-mod.1 + SPEC 048 guard

Обновление снапшота с v0.0.0-20250811.0 на пин, которого требует
sing-box после мержа 235 коммитов (upstream d620bbbf2 "Update gvisor to
20260727.0"). Прежний снапшот был взят 2026-08-04 ровно с той версии,
на которой тогда стоял апстрим; разрыв возник 2026-08-05 вместе с его
бампом.

За год апстрим-gvisor изменил ~14 000 строк в 292 файлах. Значимое для
нас — сетевой стек: tcp/connect.go (PMTU-discovery + исправление
начального RTT/RTO: раньше задержка ACK внутри стека завышала стартовый
таймаут на несколько RTT), tcp/snd.go, tcp/rcv.go, stack/conntrack.go,
stack/packet_buffer.go. Всего 30 файлов в TCP и 37 в stack.

Баг SPEC 048 апстрим НЕ исправил — проверено по коду новой версии:
handleConnecting по-прежнему проверяет состояние endpoint'а, но не ep.h,
а performHandshake так же зануляет h и отпускает мьютекс до Close().
Поэтому guard перенесён (12 строк) вместе со своим тестом (45 строк).

Red/green проверен на новой базе: без guard'а тест падает с той же
nil-паникой, что в полевом крашдампе; с ним зелёный.
This commit is contained in:
Leadaxe 2026-08-05 14:53:31 +03:00
parent ffebe42860
commit 117243aa02
293 changed files with 16413 additions and 2842 deletions

View file

@ -0,0 +1,835 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.4
// source: sandboxexec/proto/sandbox_options.proto
package sandbox_options_go_proto
import (
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type EnvVar_EnvVarPolicy int32
const (
EnvVar_ENV_VAR_POLICY_UNSPECIFIED EnvVar_EnvVarPolicy = 0
EnvVar_ENV_VAR_POLICY_FORWARD EnvVar_EnvVarPolicy = 1
EnvVar_ENV_VAR_POLICY_UNSET EnvVar_EnvVarPolicy = 2
)
// Enum value maps for EnvVar_EnvVarPolicy.
var (
EnvVar_EnvVarPolicy_name = map[int32]string{
0: "ENV_VAR_POLICY_UNSPECIFIED",
1: "ENV_VAR_POLICY_FORWARD",
2: "ENV_VAR_POLICY_UNSET",
}
EnvVar_EnvVarPolicy_value = map[string]int32{
"ENV_VAR_POLICY_UNSPECIFIED": 0,
"ENV_VAR_POLICY_FORWARD": 1,
"ENV_VAR_POLICY_UNSET": 2,
}
)
func (x EnvVar_EnvVarPolicy) Enum() *EnvVar_EnvVarPolicy {
p := new(EnvVar_EnvVarPolicy)
*p = x
return p
}
func (x EnvVar_EnvVarPolicy) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (EnvVar_EnvVarPolicy) Descriptor() protoreflect.EnumDescriptor {
return file_sandboxexec_proto_sandbox_options_proto_enumTypes[0].Descriptor()
}
func (EnvVar_EnvVarPolicy) Type() protoreflect.EnumType {
return &file_sandboxexec_proto_sandbox_options_proto_enumTypes[0]
}
func (x EnvVar_EnvVarPolicy) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use EnvVar_EnvVarPolicy.Descriptor instead.
func (EnvVar_EnvVarPolicy) EnumDescriptor() ([]byte, []int) {
return file_sandboxexec_proto_sandbox_options_proto_rawDescGZIP(), []int{3, 0}
}
type PortMapping_PortMappingType int32
const (
PortMapping_PORT_MAPPING_TYPE_TCP PortMapping_PortMappingType = 0
PortMapping_PORT_MAPPING_TYPE_UDP PortMapping_PortMappingType = 1
)
// Enum value maps for PortMapping_PortMappingType.
var (
PortMapping_PortMappingType_name = map[int32]string{
0: "PORT_MAPPING_TYPE_TCP",
1: "PORT_MAPPING_TYPE_UDP",
}
PortMapping_PortMappingType_value = map[string]int32{
"PORT_MAPPING_TYPE_TCP": 0,
"PORT_MAPPING_TYPE_UDP": 1,
}
)
func (x PortMapping_PortMappingType) Enum() *PortMapping_PortMappingType {
p := new(PortMapping_PortMappingType)
*p = x
return p
}
func (x PortMapping_PortMappingType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (PortMapping_PortMappingType) Descriptor() protoreflect.EnumDescriptor {
return file_sandboxexec_proto_sandbox_options_proto_enumTypes[1].Descriptor()
}
func (PortMapping_PortMappingType) Type() protoreflect.EnumType {
return &file_sandboxexec_proto_sandbox_options_proto_enumTypes[1]
}
func (x PortMapping_PortMappingType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use PortMapping_PortMappingType.Descriptor instead.
func (PortMapping_PortMappingType) EnumDescriptor() ([]byte, []int) {
return file_sandboxexec_proto_sandbox_options_proto_rawDescGZIP(), []int{4, 0}
}
type SandboxOptions struct {
state protoimpl.MessageState `protogen:"open.v1"`
Mounts []*Mount `protobuf:"bytes,1,rep,name=mounts,proto3" json:"mounts,omitempty"`
NetworkingOptions *NetworkingOptions `protobuf:"bytes,2,opt,name=networking_options,json=networkingOptions,proto3" json:"networking_options,omitempty"`
StdoutFile string `protobuf:"bytes,3,opt,name=stdout_file,json=stdoutFile,proto3" json:"stdout_file,omitempty"`
StderrFile string `protobuf:"bytes,4,opt,name=stderr_file,json=stderrFile,proto3" json:"stderr_file,omitempty"`
EnvVars []*EnvVar `protobuf:"bytes,5,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty"`
DebugOptions *DebugOptions `protobuf:"bytes,6,opt,name=debug_options,json=debugOptions,proto3" json:"debug_options,omitempty"`
StartTimeoutMs int64 `protobuf:"varint,7,opt,name=start_timeout_ms,json=startTimeoutMs,proto3" json:"start_timeout_ms,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SandboxOptions) Reset() {
*x = SandboxOptions{}
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SandboxOptions) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SandboxOptions) ProtoMessage() {}
func (x *SandboxOptions) ProtoReflect() protoreflect.Message {
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SandboxOptions.ProtoReflect.Descriptor instead.
func (*SandboxOptions) Descriptor() ([]byte, []int) {
return file_sandboxexec_proto_sandbox_options_proto_rawDescGZIP(), []int{0}
}
func (x *SandboxOptions) GetMounts() []*Mount {
if x != nil {
return x.Mounts
}
return nil
}
func (x *SandboxOptions) GetNetworkingOptions() *NetworkingOptions {
if x != nil {
return x.NetworkingOptions
}
return nil
}
func (x *SandboxOptions) GetStdoutFile() string {
if x != nil {
return x.StdoutFile
}
return ""
}
func (x *SandboxOptions) GetStderrFile() string {
if x != nil {
return x.StderrFile
}
return ""
}
func (x *SandboxOptions) GetEnvVars() []*EnvVar {
if x != nil {
return x.EnvVars
}
return nil
}
func (x *SandboxOptions) GetDebugOptions() *DebugOptions {
if x != nil {
return x.DebugOptions
}
return nil
}
func (x *SandboxOptions) GetStartTimeoutMs() int64 {
if x != nil {
return x.StartTimeoutMs
}
return 0
}
type NetworkingOptions struct {
state protoimpl.MessageState `protogen:"open.v1"`
EnableExternalNetworking bool `protobuf:"varint,1,opt,name=enable_external_networking,json=enableExternalNetworking,proto3" json:"enable_external_networking,omitempty"`
PortMappings []*PortMapping `protobuf:"bytes,2,rep,name=port_mappings,json=portMappings,proto3" json:"port_mappings,omitempty"`
DomainAllowlist []string `protobuf:"bytes,3,rep,name=domain_allowlist,json=domainAllowlist,proto3" json:"domain_allowlist,omitempty"`
DomainDenylist []string `protobuf:"bytes,4,rep,name=domain_denylist,json=domainDenylist,proto3" json:"domain_denylist,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *NetworkingOptions) Reset() {
*x = NetworkingOptions{}
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *NetworkingOptions) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NetworkingOptions) ProtoMessage() {}
func (x *NetworkingOptions) ProtoReflect() protoreflect.Message {
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NetworkingOptions.ProtoReflect.Descriptor instead.
func (*NetworkingOptions) Descriptor() ([]byte, []int) {
return file_sandboxexec_proto_sandbox_options_proto_rawDescGZIP(), []int{1}
}
func (x *NetworkingOptions) GetEnableExternalNetworking() bool {
if x != nil {
return x.EnableExternalNetworking
}
return false
}
func (x *NetworkingOptions) GetPortMappings() []*PortMapping {
if x != nil {
return x.PortMappings
}
return nil
}
func (x *NetworkingOptions) GetDomainAllowlist() []string {
if x != nil {
return x.DomainAllowlist
}
return nil
}
func (x *NetworkingOptions) GetDomainDenylist() []string {
if x != nil {
return x.DomainDenylist
}
return nil
}
type DebugOptions struct {
state protoimpl.MessageState `protogen:"open.v1"`
EnableDebugLogs string `protobuf:"bytes,1,opt,name=enable_debug_logs,json=enableDebugLogs,proto3" json:"enable_debug_logs,omitempty"`
DebugLogFile string `protobuf:"bytes,2,opt,name=debug_log_file,json=debugLogFile,proto3" json:"debug_log_file,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DebugOptions) Reset() {
*x = DebugOptions{}
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DebugOptions) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DebugOptions) ProtoMessage() {}
func (x *DebugOptions) ProtoReflect() protoreflect.Message {
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DebugOptions.ProtoReflect.Descriptor instead.
func (*DebugOptions) Descriptor() ([]byte, []int) {
return file_sandboxexec_proto_sandbox_options_proto_rawDescGZIP(), []int{2}
}
func (x *DebugOptions) GetEnableDebugLogs() string {
if x != nil {
return x.EnableDebugLogs
}
return ""
}
func (x *DebugOptions) GetDebugLogFile() string {
if x != nil {
return x.DebugLogFile
}
return ""
}
type EnvVar struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// Types that are valid to be assigned to PolicyOrValue:
//
// *EnvVar_Value
// *EnvVar_Policy
PolicyOrValue isEnvVar_PolicyOrValue `protobuf_oneof:"policy_or_value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EnvVar) Reset() {
*x = EnvVar{}
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EnvVar) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EnvVar) ProtoMessage() {}
func (x *EnvVar) ProtoReflect() protoreflect.Message {
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EnvVar.ProtoReflect.Descriptor instead.
func (*EnvVar) Descriptor() ([]byte, []int) {
return file_sandboxexec_proto_sandbox_options_proto_rawDescGZIP(), []int{3}
}
func (x *EnvVar) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *EnvVar) GetPolicyOrValue() isEnvVar_PolicyOrValue {
if x != nil {
return x.PolicyOrValue
}
return nil
}
func (x *EnvVar) GetValue() string {
if x != nil {
if x, ok := x.PolicyOrValue.(*EnvVar_Value); ok {
return x.Value
}
}
return ""
}
func (x *EnvVar) GetPolicy() EnvVar_EnvVarPolicy {
if x != nil {
if x, ok := x.PolicyOrValue.(*EnvVar_Policy); ok {
return x.Policy
}
}
return EnvVar_ENV_VAR_POLICY_UNSPECIFIED
}
type isEnvVar_PolicyOrValue interface {
isEnvVar_PolicyOrValue()
}
type EnvVar_Value struct {
Value string `protobuf:"bytes,2,opt,name=value,proto3,oneof"`
}
type EnvVar_Policy struct {
Policy EnvVar_EnvVarPolicy `protobuf:"varint,3,opt,name=policy,proto3,enum=cloud_gvisor_sandboxexec.EnvVar_EnvVarPolicy,oneof"`
}
func (*EnvVar_Value) isEnvVar_PolicyOrValue() {}
func (*EnvVar_Policy) isEnvVar_PolicyOrValue() {}
type PortMapping struct {
state protoimpl.MessageState `protogen:"open.v1"`
SandboxPort uint32 `protobuf:"varint,1,opt,name=sandbox_port,json=sandboxPort,proto3" json:"sandbox_port,omitempty"`
HostPort uint32 `protobuf:"varint,2,opt,name=host_port,json=hostPort,proto3" json:"host_port,omitempty"`
PortMappingType PortMapping_PortMappingType `protobuf:"varint,3,opt,name=port_mapping_type,json=portMappingType,proto3,enum=cloud_gvisor_sandboxexec.PortMapping_PortMappingType" json:"port_mapping_type,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PortMapping) Reset() {
*x = PortMapping{}
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PortMapping) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PortMapping) ProtoMessage() {}
func (x *PortMapping) ProtoReflect() protoreflect.Message {
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PortMapping.ProtoReflect.Descriptor instead.
func (*PortMapping) Descriptor() ([]byte, []int) {
return file_sandboxexec_proto_sandbox_options_proto_rawDescGZIP(), []int{4}
}
func (x *PortMapping) GetSandboxPort() uint32 {
if x != nil {
return x.SandboxPort
}
return 0
}
func (x *PortMapping) GetHostPort() uint32 {
if x != nil {
return x.HostPort
}
return 0
}
func (x *PortMapping) GetPortMappingType() PortMapping_PortMappingType {
if x != nil {
return x.PortMappingType
}
return PortMapping_PORT_MAPPING_TYPE_TCP
}
type MountOptions struct {
state protoimpl.MessageState `protogen:"open.v1"`
Passthrough bool `protobuf:"varint,1,opt,name=passthrough,proto3" json:"passthrough,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *MountOptions) Reset() {
*x = MountOptions{}
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *MountOptions) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MountOptions) ProtoMessage() {}
func (x *MountOptions) ProtoReflect() protoreflect.Message {
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MountOptions.ProtoReflect.Descriptor instead.
func (*MountOptions) Descriptor() ([]byte, []int) {
return file_sandboxexec_proto_sandbox_options_proto_rawDescGZIP(), []int{5}
}
func (x *MountOptions) GetPassthrough() bool {
if x != nil {
return x.Passthrough
}
return false
}
type Mount struct {
state protoimpl.MessageState `protogen:"open.v1"`
Target string `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"`
// Types that are valid to be assigned to Mount:
//
// *Mount_HostMount
// *Mount_SandboxTmpfsMount
Mount isMount_Mount `protobuf_oneof:"mount"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Mount) Reset() {
*x = Mount{}
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Mount) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Mount) ProtoMessage() {}
func (x *Mount) ProtoReflect() protoreflect.Message {
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Mount.ProtoReflect.Descriptor instead.
func (*Mount) Descriptor() ([]byte, []int) {
return file_sandboxexec_proto_sandbox_options_proto_rawDescGZIP(), []int{6}
}
func (x *Mount) GetTarget() string {
if x != nil {
return x.Target
}
return ""
}
func (x *Mount) GetMount() isMount_Mount {
if x != nil {
return x.Mount
}
return nil
}
func (x *Mount) GetHostMount() *HostMount {
if x != nil {
if x, ok := x.Mount.(*Mount_HostMount); ok {
return x.HostMount
}
}
return nil
}
func (x *Mount) GetSandboxTmpfsMount() *SandboxTmpfsMount {
if x != nil {
if x, ok := x.Mount.(*Mount_SandboxTmpfsMount); ok {
return x.SandboxTmpfsMount
}
}
return nil
}
type isMount_Mount interface {
isMount_Mount()
}
type Mount_HostMount struct {
HostMount *HostMount `protobuf:"bytes,2,opt,name=host_mount,json=hostMount,proto3,oneof"`
}
type Mount_SandboxTmpfsMount struct {
SandboxTmpfsMount *SandboxTmpfsMount `protobuf:"bytes,3,opt,name=sandbox_tmpfs_mount,json=sandboxTmpfsMount,proto3,oneof"`
}
func (*Mount_HostMount) isMount_Mount() {}
func (*Mount_SandboxTmpfsMount) isMount_Mount() {}
type HostMount struct {
state protoimpl.MessageState `protogen:"open.v1"`
HostPath string `protobuf:"bytes,1,opt,name=host_path,json=hostPath,proto3" json:"host_path,omitempty"`
Options *MountOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HostMount) Reset() {
*x = HostMount{}
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HostMount) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HostMount) ProtoMessage() {}
func (x *HostMount) ProtoReflect() protoreflect.Message {
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HostMount.ProtoReflect.Descriptor instead.
func (*HostMount) Descriptor() ([]byte, []int) {
return file_sandboxexec_proto_sandbox_options_proto_rawDescGZIP(), []int{7}
}
func (x *HostMount) GetHostPath() string {
if x != nil {
return x.HostPath
}
return ""
}
func (x *HostMount) GetOptions() *MountOptions {
if x != nil {
return x.Options
}
return nil
}
type SandboxTmpfsMount struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SandboxTmpfsMount) Reset() {
*x = SandboxTmpfsMount{}
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SandboxTmpfsMount) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SandboxTmpfsMount) ProtoMessage() {}
func (x *SandboxTmpfsMount) ProtoReflect() protoreflect.Message {
mi := &file_sandboxexec_proto_sandbox_options_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SandboxTmpfsMount.ProtoReflect.Descriptor instead.
func (*SandboxTmpfsMount) Descriptor() ([]byte, []int) {
return file_sandboxexec_proto_sandbox_options_proto_rawDescGZIP(), []int{8}
}
var File_sandboxexec_proto_sandbox_options_proto protoreflect.FileDescriptor
const file_sandboxexec_proto_sandbox_options_proto_rawDesc = "" +
"\n" +
"'sandboxexec/proto/sandbox_options.proto\x12\x18cloud_gvisor_sandboxexec\"\x9b\x03\n" +
"\x0eSandboxOptions\x127\n" +
"\x06mounts\x18\x01 \x03(\v2\x1f.cloud_gvisor_sandboxexec.MountR\x06mounts\x12Z\n" +
"\x12networking_options\x18\x02 \x01(\v2+.cloud_gvisor_sandboxexec.NetworkingOptionsR\x11networkingOptions\x12\x1f\n" +
"\vstdout_file\x18\x03 \x01(\tR\n" +
"stdoutFile\x12\x1f\n" +
"\vstderr_file\x18\x04 \x01(\tR\n" +
"stderrFile\x12;\n" +
"\benv_vars\x18\x05 \x03(\v2 .cloud_gvisor_sandboxexec.EnvVarR\aenvVars\x12K\n" +
"\rdebug_options\x18\x06 \x01(\v2&.cloud_gvisor_sandboxexec.DebugOptionsR\fdebugOptions\x12(\n" +
"\x10start_timeout_ms\x18\a \x01(\x03R\x0estartTimeoutMs\"\xf1\x01\n" +
"\x11NetworkingOptions\x12<\n" +
"\x1aenable_external_networking\x18\x01 \x01(\bR\x18enableExternalNetworking\x12J\n" +
"\rport_mappings\x18\x02 \x03(\v2%.cloud_gvisor_sandboxexec.PortMappingR\fportMappings\x12)\n" +
"\x10domain_allowlist\x18\x03 \x03(\tR\x0fdomainAllowlist\x12'\n" +
"\x0fdomain_denylist\x18\x04 \x03(\tR\x0edomainDenylist\"`\n" +
"\fDebugOptions\x12*\n" +
"\x11enable_debug_logs\x18\x01 \x01(\tR\x0fenableDebugLogs\x12$\n" +
"\x0edebug_log_file\x18\x02 \x01(\tR\fdebugLogFile\"\xf6\x01\n" +
"\x06EnvVar\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" +
"\x05value\x18\x02 \x01(\tH\x00R\x05value\x12G\n" +
"\x06policy\x18\x03 \x01(\x0e2-.cloud_gvisor_sandboxexec.EnvVar.EnvVarPolicyH\x00R\x06policy\"d\n" +
"\fEnvVarPolicy\x12\x1e\n" +
"\x1aENV_VAR_POLICY_UNSPECIFIED\x10\x00\x12\x1a\n" +
"\x16ENV_VAR_POLICY_FORWARD\x10\x01\x12\x18\n" +
"\x14ENV_VAR_POLICY_UNSET\x10\x02B\x11\n" +
"\x0fpolicy_or_value\"\xf9\x01\n" +
"\vPortMapping\x12!\n" +
"\fsandbox_port\x18\x01 \x01(\rR\vsandboxPort\x12\x1b\n" +
"\thost_port\x18\x02 \x01(\rR\bhostPort\x12a\n" +
"\x11port_mapping_type\x18\x03 \x01(\x0e25.cloud_gvisor_sandboxexec.PortMapping.PortMappingTypeR\x0fportMappingType\"G\n" +
"\x0fPortMappingType\x12\x19\n" +
"\x15PORT_MAPPING_TYPE_TCP\x10\x00\x12\x19\n" +
"\x15PORT_MAPPING_TYPE_UDP\x10\x01\"0\n" +
"\fMountOptions\x12 \n" +
"\vpassthrough\x18\x01 \x01(\bR\vpassthrough\"\xcd\x01\n" +
"\x05Mount\x12\x16\n" +
"\x06target\x18\x01 \x01(\tR\x06target\x12D\n" +
"\n" +
"host_mount\x18\x02 \x01(\v2#.cloud_gvisor_sandboxexec.HostMountH\x00R\thostMount\x12]\n" +
"\x13sandbox_tmpfs_mount\x18\x03 \x01(\v2+.cloud_gvisor_sandboxexec.SandboxTmpfsMountH\x00R\x11sandboxTmpfsMountB\a\n" +
"\x05mount\"j\n" +
"\tHostMount\x12\x1b\n" +
"\thost_path\x18\x01 \x01(\tR\bhostPath\x12@\n" +
"\aoptions\x18\x02 \x01(\v2&.cloud_gvisor_sandboxexec.MountOptionsR\aoptions\"\x13\n" +
"\x11SandboxTmpfsMountb\x06proto3"
var (
file_sandboxexec_proto_sandbox_options_proto_rawDescOnce sync.Once
file_sandboxexec_proto_sandbox_options_proto_rawDescData []byte
)
func file_sandboxexec_proto_sandbox_options_proto_rawDescGZIP() []byte {
file_sandboxexec_proto_sandbox_options_proto_rawDescOnce.Do(func() {
file_sandboxexec_proto_sandbox_options_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_sandboxexec_proto_sandbox_options_proto_rawDesc), len(file_sandboxexec_proto_sandbox_options_proto_rawDesc)))
})
return file_sandboxexec_proto_sandbox_options_proto_rawDescData
}
var (
file_sandboxexec_proto_sandbox_options_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
file_sandboxexec_proto_sandbox_options_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
file_sandboxexec_proto_sandbox_options_proto_goTypes = []any{
(EnvVar_EnvVarPolicy)(0), // 0: cloud_gvisor_sandboxexec.EnvVar.EnvVarPolicy
(PortMapping_PortMappingType)(0), // 1: cloud_gvisor_sandboxexec.PortMapping.PortMappingType
(*SandboxOptions)(nil), // 2: cloud_gvisor_sandboxexec.SandboxOptions
(*NetworkingOptions)(nil), // 3: cloud_gvisor_sandboxexec.NetworkingOptions
(*DebugOptions)(nil), // 4: cloud_gvisor_sandboxexec.DebugOptions
(*EnvVar)(nil), // 5: cloud_gvisor_sandboxexec.EnvVar
(*PortMapping)(nil), // 6: cloud_gvisor_sandboxexec.PortMapping
(*MountOptions)(nil), // 7: cloud_gvisor_sandboxexec.MountOptions
(*Mount)(nil), // 8: cloud_gvisor_sandboxexec.Mount
(*HostMount)(nil), // 9: cloud_gvisor_sandboxexec.HostMount
(*SandboxTmpfsMount)(nil), // 10: cloud_gvisor_sandboxexec.SandboxTmpfsMount
}
)
var file_sandboxexec_proto_sandbox_options_proto_depIdxs = []int32{
8, // 0: cloud_gvisor_sandboxexec.SandboxOptions.mounts:type_name -> cloud_gvisor_sandboxexec.Mount
3, // 1: cloud_gvisor_sandboxexec.SandboxOptions.networking_options:type_name -> cloud_gvisor_sandboxexec.NetworkingOptions
5, // 2: cloud_gvisor_sandboxexec.SandboxOptions.env_vars:type_name -> cloud_gvisor_sandboxexec.EnvVar
4, // 3: cloud_gvisor_sandboxexec.SandboxOptions.debug_options:type_name -> cloud_gvisor_sandboxexec.DebugOptions
6, // 4: cloud_gvisor_sandboxexec.NetworkingOptions.port_mappings:type_name -> cloud_gvisor_sandboxexec.PortMapping
0, // 5: cloud_gvisor_sandboxexec.EnvVar.policy:type_name -> cloud_gvisor_sandboxexec.EnvVar.EnvVarPolicy
1, // 6: cloud_gvisor_sandboxexec.PortMapping.port_mapping_type:type_name -> cloud_gvisor_sandboxexec.PortMapping.PortMappingType
9, // 7: cloud_gvisor_sandboxexec.Mount.host_mount:type_name -> cloud_gvisor_sandboxexec.HostMount
10, // 8: cloud_gvisor_sandboxexec.Mount.sandbox_tmpfs_mount:type_name -> cloud_gvisor_sandboxexec.SandboxTmpfsMount
7, // 9: cloud_gvisor_sandboxexec.HostMount.options:type_name -> cloud_gvisor_sandboxexec.MountOptions
10, // [10:10] is the sub-list for method output_type
10, // [10:10] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension extendee
0, // [0:10] is the sub-list for field type_name
}
func init() { file_sandboxexec_proto_sandbox_options_proto_init() }
func file_sandboxexec_proto_sandbox_options_proto_init() {
if File_sandboxexec_proto_sandbox_options_proto != nil {
return
}
file_sandboxexec_proto_sandbox_options_proto_msgTypes[3].OneofWrappers = []any{
(*EnvVar_Value)(nil),
(*EnvVar_Policy)(nil),
}
file_sandboxexec_proto_sandbox_options_proto_msgTypes[6].OneofWrappers = []any{
(*Mount_HostMount)(nil),
(*Mount_SandboxTmpfsMount)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_sandboxexec_proto_sandbox_options_proto_rawDesc), len(file_sandboxexec_proto_sandbox_options_proto_rawDesc)),
NumEnums: 2,
NumMessages: 9,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_sandboxexec_proto_sandbox_options_proto_goTypes,
DependencyIndexes: file_sandboxexec_proto_sandbox_options_proto_depIdxs,
EnumInfos: file_sandboxexec_proto_sandbox_options_proto_enumTypes,
MessageInfos: file_sandboxexec_proto_sandbox_options_proto_msgTypes,
}.Build()
File_sandboxexec_proto_sandbox_options_proto = out.File
file_sandboxexec_proto_sandbox_options_proto_goTypes = nil
file_sandboxexec_proto_sandbox_options_proto_depIdxs = nil
}

165
sandboxexec/sandbox/oci.go Normal file
View file

@ -0,0 +1,165 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sandbox
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// BundleConfig holds configuration for creating an OCI bundle.
type BundleConfig struct {
ID string
RuntimeDir string
EnableNetworking bool
Mounts []Mount
Env []string
Annotations map[string]string
WorkingDir string
Hostname string
}
// NewBundle creates a temporary OCI bundle on the fly with the given configuration.
func NewBundle(cfg BundleConfig) (string, error) {
// Create a bundle directory for the sandbox.
bundleDir := filepath.Join(cfg.RuntimeDir, cfg.ID)
rootfsDir := filepath.Join(bundleDir, "rootfs")
if err := os.MkdirAll(rootfsDir, 0o755); err != nil {
return "", fmt.Errorf("failed to create bundle directories: %w", err)
}
// Define the OCI Specification programmatically.
namespaces := []specs.LinuxNamespace{
{Type: specs.PIDNamespace},
{Type: specs.MountNamespace},
{Type: specs.UTSNamespace},
{Type: specs.IPCNamespace},
}
if os.Geteuid() != 0 {
namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.UserNamespace})
}
if cfg.EnableNetworking {
namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.NetworkNamespace})
}
spec := &specs.Spec{
Version: "1.0.0",
Annotations: cfg.Annotations,
Root: &specs.Root{
Path: "rootfs",
Readonly: false,
},
Process: &specs.Process{
Terminal: false,
User: specs.User{UID: 0, GID: 0},
// Keeps the sandbox alive in the background.
Args: []string{"sleep", "infinity"},
Cwd: cfg.WorkingDir,
},
Mounts: []specs.Mount{
// Mandatory Linux API Filesystems
{Destination: "/proc", Type: "proc", Source: "proc"},
{Destination: "/dev", Type: "tmpfs", Source: "tmpfs"},
},
// enable basic namespaces for gVisor.
Linux: &specs.Linux{
Namespaces: namespaces,
},
Hostname: cfg.Hostname,
}
baseEnv := []string{"PATH=/bin:/usr/bin:/usr/local/bin"}
spec.Process.Env = append(baseEnv, cfg.Env...)
if os.Geteuid() != 0 {
spec.Linux.UIDMappings = []specs.LinuxIDMapping{
{ContainerID: 0, HostID: uint32(os.Geteuid()), Size: 1},
}
spec.Linux.GIDMappings = []specs.LinuxIDMapping{
{ContainerID: 0, HostID: uint32(os.Getegid()), Size: 1},
}
}
// Map host binaries & libraries as readonly. The binaries will be
// executed in gVisor sandbox, not on the host.
for _, p := range []string{"/bin", "/usr", "/lib", "/lib64", "/etc/alternatives"} {
if _, err := os.Stat(p); err == nil {
opts := []string{"rbind", "ro", "nosuid", "nodev"}
if p == "/etc/alternatives" {
opts = []string{"rbind", "ro"}
}
spec.Mounts = append(spec.Mounts, specs.Mount{
Destination: p,
Type: "bind",
Source: p,
Options: opts,
})
}
}
// Add custom mounts. Custom mounts overriding default host mounts create duplicate OCI
// entries. The later entry overrides the earlier one, as expected by OCI specs.
for _, m := range cfg.Mounts {
switch m.Type {
case MountTypeBind:
opts := []string{"rbind"}
if m.ReadOnly {
opts = append(opts, "ro")
} else {
opts = append(opts, "rw")
}
spec.Mounts = append(spec.Mounts, specs.Mount{
Destination: filepath.Clean(m.Destination),
Source: filepath.Clean(m.Source),
Type: "bind",
Options: opts,
})
case MountTypeTmpfs:
spec.Mounts = append(spec.Mounts, specs.Mount{
Destination: filepath.Clean(m.Destination),
Source: "tmpfs",
Type: "tmpfs",
})
case MountTypeProc:
spec.Mounts = append(spec.Mounts, specs.Mount{
Destination: filepath.Clean(m.Destination),
Source: "proc",
Type: "proc",
})
}
}
// Write the spec to config.json
configPath := filepath.Join(bundleDir, "config.json")
configFile, err := os.Create(configPath)
if err != nil {
return "", fmt.Errorf("failed to create config.json: %w", err)
}
defer configFile.Close()
encoder := json.NewEncoder(configFile)
encoder.SetIndent("", " ")
if err := encoder.Encode(spec); err != nil {
return "", fmt.Errorf("failed to encode config.json: %w", err)
}
return bundleDir, nil
}

View file

@ -0,0 +1,528 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package sandbox provides a simple Go API for creating gVisor sandbox
// and executing commands in the sandbox.
package sandbox
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// Options holds the configuration for a Sandbox.
type Options struct {
runtimeDir string
id string
enableNetworking bool
mounts []Mount
snapshot *Snapshot
env []string
err error
workingDir string
hostname string
}
// Option configures the Options struct.
type Option func(*Options)
// MountType represents the type of a mount point inside the sandbox.
type MountType int
const (
// MountTypeBind represents a host bind mount.
MountTypeBind MountType = iota
// MountTypeTmpfs represents an in-memory tmpfs mount.
MountTypeTmpfs
// MountTypeProc represents a procfs mount.
MountTypeProc
)
// Mount holds settings for a custom host bind directory or in-memory mount.
type Mount struct {
Source string
Destination string
Type MountType
ReadOnly bool
}
// WithRuntimeDir sets a custom runtime directory where bundle and state files are written.
func WithRuntimeDir(runtimeDir string) Option {
return func(o *Options) {
o.runtimeDir = runtimeDir
}
}
// WithID sets a specific sandbox ID. If not set, a unique ID will be generated automatically.
func WithID(id string) Option {
return func(o *Options) {
o.id = id
}
}
// WithNetworking configures whether networking is enabled inside the sandbox.
func WithNetworking(enabled bool) Option {
return func(o *Options) {
o.enableNetworking = enabled
}
}
// WithBindMount adds a custom bind mount from host's source path to the sandbox's destination path.
func WithBindMount(source, destination string, readOnly bool) Option {
return func(o *Options) {
o.mounts = append(o.mounts, Mount{
Source: filepath.Clean(source),
Destination: filepath.Clean(destination),
Type: MountTypeBind,
ReadOnly: readOnly,
})
}
}
// WithTmpfsMount adds an in-memory tmpfs filesystem at the destination path inside the sandbox.
func WithTmpfsMount(destination string) Option {
return func(o *Options) {
o.mounts = append(o.mounts, Mount{
Destination: filepath.Clean(destination),
Type: MountTypeTmpfs,
})
}
}
// WithProcMount adds a procfs mount at the destination path inside the sandbox.
func WithProcMount(destination string) Option {
return func(o *Options) {
o.mounts = append(o.mounts, Mount{
Destination: filepath.Clean(destination),
Type: MountTypeProc,
})
}
}
// WithHostname sets the hostname for the sandbox.
func WithHostname(hostname string) Option {
return func(o *Options) {
o.hostname = hostname
}
}
// WithSnapshot configures the sandbox to restore state from the given snapshot.
// The sandbox automatically reads the snapshot metadata to determine if it is a
// full Checkpoint/Restore, Filesystem snapshot, or Rootfs Tar snapshot.
func WithSnapshot(snapshot *Snapshot) Option {
return func(o *Options) {
o.snapshot = snapshot
}
}
// WithEnv sets one or more environment variables in the sandbox process.
// Each env string must be in the "KEY=VALUE" format.
func WithEnv(envs ...string) Option {
return func(o *Options) {
for _, env := range envs {
if !strings.Contains(env, "=") {
o.err = fmt.Errorf("invalid environment variable format, expected KEY=VALUE: %q", env)
return
}
}
o.env = append(o.env, envs...)
}
}
// WithWorkingDir sets the current working directory for the sandbox process.
// If the path is relative, it will be resolved as an absolute path from the root directory "/".
// This is not a bind mount; it is simply setting the cwd inside the sandbox process.
// Defaults to "/".
func WithWorkingDir(cwd string) Option {
return func(o *Options) {
if cwd == "" {
o.err = fmt.Errorf("working directory cannot be empty")
return
}
// ensure absolute path inside sandbox
if !filepath.IsAbs(cwd) {
cwd = filepath.Join("/", cwd)
}
cwd = filepath.Clean(cwd)
o.workingDir = cwd
}
}
// Sandbox represents a running gVisor sandbox where applications
// run inside.
type Sandbox struct {
id string
bundleDir string
runscPath string
rootState string
}
// newID returns a unique ID for the sandbox.
func newID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
// rand.Read never returns an error, and always fills b entirely.
panic(fmt.Sprintf("failed to generate random bytes for sandbox ID: %v", err))
}
return fmt.Sprintf("%x", b)
}
// Look for runsc binary from the environment variable RUNSC_PATH,
// then in system PATH.
func runscPath() string {
if path := os.Getenv("RUNSC_PATH"); path != "" {
return path
}
path, err := exec.LookPath("runsc")
if err == nil {
return path
}
panic("runsc binary is not found")
}
// New spawns a new sandbox as a subprocess, the sandbox
// will be started and running in detached mode.
func New(ctx context.Context, opts ...Option) (*Sandbox, error) {
options := Options{
enableNetworking: true,
workingDir: "/",
}
for _, o := range opts {
o(&options)
}
if options.err != nil {
return nil, options.err
}
if options.runtimeDir == "" {
dir, err := os.MkdirTemp("", "gvisor-sandbox-*")
if err != nil {
return nil, fmt.Errorf("failed to create runtime directory: %v", err)
}
options.runtimeDir = dir
}
if options.id == "" {
options.id = newID()
}
if os.Geteuid() != 0 && options.enableNetworking {
return nil, fmt.Errorf("enabling networking requires running as root")
}
runDir := options.runtimeDir
stateDir := filepath.Join(runDir, "state")
if err := os.MkdirAll(stateDir, 0o700); err != nil {
return nil, fmt.Errorf("failed to create sandbox state directory: %v", err)
}
// Verify that the state directory actually has 0700 permissions.
fi, err := os.Stat(stateDir)
if err != nil {
return nil, fmt.Errorf("failed to stat sandbox state directory: %v", err)
}
if fi.Mode().Perm() != 0o700 {
return nil, fmt.Errorf("sandbox state directory has incorrect permissions: got %v, want %v", fi.Mode().Perm(), os.FileMode(0o700))
}
var annotations map[string]string
var globalFlags []string
var runFlags []string
var isCheckpointRestore bool
var checkpointRestoreDir string
if options.snapshot != nil {
store := options.snapshot.Storage
snapshotID := options.snapshot.ID
if store == nil {
return nil, fmt.Errorf("no snapshot storage configured for restore")
}
// Fetch metadata.json from store.
metaReader, err := store.GetReader(ctx, snapshotID, MetadataAsset)
if err != nil {
return nil, fmt.Errorf("failed to read snapshot metadata: %w", err)
}
defer metaReader.Close()
var meta SnapshotMetadata
if err := json.NewDecoder(metaReader).Decode(&meta); err != nil {
return nil, fmt.Errorf("failed to parse snapshot metadata: %w", err)
}
// Perform restore based on type.
switch meta.Type {
case RootfsTarSnapshot:
tarPath, err := readRootfsTar(ctx, snapshotID, store)
if err != nil {
return nil, err
}
defer os.Remove(tarPath)
annotations = map[string]string{
"dev.gvisor.tar.rootfs.upper": tarPath,
}
globalFlags = append(globalFlags, "--allow-rootfs-tar-annotation")
case FilesystemSnapshot:
fsRestoreDir := filepath.Join(stateDir, "fs-restore")
if err := os.MkdirAll(fsRestoreDir, 0o700); err != nil {
return nil, err
}
// TODO: List assets in store and download all filesystem image assets to fsRestoreDir.
runFlags = append(runFlags, fmt.Sprintf("--fs-restore-image-path=%s", fsRestoreDir))
case CheckpointRestore:
checkpointRestoreDir = filepath.Join(stateDir, "checkpoint-restore")
if err := os.MkdirAll(checkpointRestoreDir, 0o700); err != nil {
return nil, err
}
// TODO: List assets in store and download all checkpoint image assets to checkpointRestoreDir.
isCheckpointRestore = true
}
}
bundleDir, err := NewBundle(BundleConfig{
ID: options.id,
RuntimeDir: runDir,
EnableNetworking: options.enableNetworking,
Mounts: options.mounts,
Env: options.env,
Annotations: annotations,
WorkingDir: options.workingDir,
Hostname: options.hostname,
})
if err != nil {
return nil, fmt.Errorf("failed to create OCI bundle: %v", err)
}
sb := &Sandbox{
id: options.id,
bundleDir: bundleDir,
runscPath: runscPath(),
rootState: stateDir,
}
// Launch the sandbox in detached mode via os/exec.
args := []string{"--root", sb.rootState}
if os.Geteuid() != 0 {
args = append(args, "--ignore-cgroups")
}
if !options.enableNetworking {
args = append(args, "--network=none")
}
args = append(args, globalFlags...)
if isCheckpointRestore {
args = append(args, "restore", "--image-path", checkpointRestoreDir, "--detach", sb.id)
} else {
args = append(args, "run")
args = append(args, runFlags...)
args = append(args, "--bundle", sb.bundleDir, "--detach", sb.id)
}
cmd := exec.CommandContext(ctx, sb.runscPath, args...)
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("failed to create sandbox via subprocess: %v", err)
}
return sb, nil
}
// Exec runs the given command inside the running sandbox and returns the output.
func (s *Sandbox) Exec(ctx context.Context, cmd string, opts ...string) (stdout string, stderr string, err error) {
args := append([]string{"--root", s.rootState, "exec", s.id, cmd}, opts...)
commandContext := exec.CommandContext(ctx, s.runscPath, args...)
var stdoutBuf, stderrBuf bytes.Buffer
commandContext.Stdout = io.MultiWriter(&stdoutBuf)
commandContext.Stderr = io.MultiWriter(&stderrBuf)
err = commandContext.Run()
if err != nil {
return "", stderrBuf.String(), fmt.Errorf("exec failed: %v", err)
}
return stdoutBuf.String(), stderrBuf.String(), err
}
// Close kills the sandbox processes and cleans up the state directory.
func (s *Sandbox) Close(ctx context.Context) error {
killArgs := []string{"--root", s.rootState, "kill", s.id, "SIGKILL"}
_ = exec.CommandContext(ctx, s.runscPath, killArgs...).Run()
deleteArgs := []string{"--root", s.rootState, "delete", "--force", s.id}
if err := exec.CommandContext(ctx, s.runscPath, deleteArgs...).Run(); err != nil {
return fmt.Errorf("failed to clean up sandbox state: %v", err)
}
if err := os.RemoveAll(s.bundleDir); err != nil {
return fmt.Errorf("failed to clean up sandbox bundle directory: %v", err)
}
if err := os.RemoveAll(s.rootState); err != nil {
return fmt.Errorf("failed to clean up sandbox state directory: %v", err)
}
return nil
}
// Bundle returns the path to the OCI bundle directory for this sandbox.
func (s *Sandbox) Bundle() string {
return s.bundleDir
}
// SnapshotOptions holds configuration for taking a snapshot.
type SnapshotOptions struct {
LeaveRunning bool
}
// SnapshotOption configures SnapshotOptions.
type SnapshotOption func(*SnapshotOptions)
// WithLeaveRunning keeps the sandbox running after taking the snapshot.
func WithLeaveRunning(leaveRunning bool) SnapshotOption {
return func(o *SnapshotOptions) {
o.LeaveRunning = leaveRunning
}
}
func newSnapshotID() SnapshotID {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(fmt.Sprintf("failed to generate random bytes for snapshot ID: %v", err))
}
return SnapshotID(fmt.Sprintf("snap-%x", b))
}
// Snapshot serializes and saves the sandbox state to storage, returning the generated snapshot.
// Depending on the snapshotType, it will perform a full Checkpoint, a Filesystem Snapshot, or a Rootfs Tar Snapshot.
// It also automatically generates and writes "metadata.json" into the storage.
func (s *Sandbox) Snapshot(ctx context.Context, snapshotType SnapshotType, storage SnapshotStorage, opts ...SnapshotOption) (*Snapshot, error) {
options := SnapshotOptions{
LeaveRunning: false, // Default is false.
}
for _, o := range opts {
o(&options)
}
snapshotID := newSnapshotID()
switch snapshotType {
case RootfsTarSnapshot:
if err := s.snapshotRootfsTar(ctx, snapshotID, storage); err != nil {
return nil, err
}
case FilesystemSnapshot:
// TODO: Run `runsc fscheckpoint --image-path=<localTempDir> [--leave-running] <sandboxID>`.
// TODO: Walk `<localTempDir>` and upload each file to storage.
case CheckpointRestore:
// TODO: Run `runsc checkpoint --image-path=<localTempDir> [--leave-running] <sandboxID>`.
// TODO: Walk `<localTempDir>` and upload each file to storage.
}
meta := SnapshotMetadata{
Type: snapshotType,
CreatedAt: time.Now().UTC().Format(time.RFC3339),
}
metaWriter, err := storage.PutWriter(ctx, snapshotID, MetadataAsset)
if err != nil {
return nil, fmt.Errorf("failed to create metadata.json in storage: %w", err)
}
defer metaWriter.Close()
if err := json.NewEncoder(metaWriter).Encode(&meta); err != nil {
return nil, fmt.Errorf("failed to write metadata.json to storage: %w", err)
}
return &Snapshot{
ID: snapshotID,
Storage: storage,
}, nil
}
func (s *Sandbox) snapshotRootfsTar(ctx context.Context, snapshotID SnapshotID, storage SnapshotStorage) error {
tarFile, err := os.CreateTemp(os.TempDir(), "rootfs-*.tar")
if err != nil {
return fmt.Errorf("failed to create temp tar file: %w", err)
}
tarPath := tarFile.Name()
tarFile.Close()
defer os.Remove(tarPath)
cmd := exec.CommandContext(ctx, s.runscPath, "--root", s.rootState, "tar", "rootfs-upper", "--file", tarPath, s.id)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("runsc tar failed: %v (stderr: %q)", err, stderr.String())
}
localFile, err := os.Open(tarPath)
if err != nil {
return fmt.Errorf("failed to open temp tar file: %w", err)
}
defer localFile.Close()
storageWriter, err := storage.PutWriter(ctx, snapshotID, RootfsAsset)
if err != nil {
return fmt.Errorf("failed to create storage writer: %w", err)
}
defer storageWriter.Close()
if _, err := io.Copy(storageWriter, localFile); err != nil {
return fmt.Errorf("failed to upload rootfs tar: %w", err)
}
return nil
}
func readRootfsTar(ctx context.Context, snapshotID SnapshotID, store SnapshotStorage) (string, error) {
tarFile, err := os.CreateTemp(os.TempDir(), "rootfs-*.tar")
if err != nil {
return "", fmt.Errorf("failed to create temp tar file: %w", err)
}
tarPath := tarFile.Name()
defer tarFile.Close()
cleanup := true
defer func() {
if cleanup {
os.Remove(tarPath)
}
}()
storageReader, err := store.GetReader(ctx, snapshotID, RootfsAsset)
if err != nil {
return "", fmt.Errorf("failed to get rootfs reader from storage: %w", err)
}
defer storageReader.Close()
if _, err := io.Copy(tarFile, storageReader); err != nil {
return "", fmt.Errorf("failed to download rootfs asset: %w", err)
}
cleanup = false
return tarPath, nil
}

View file

@ -0,0 +1,3 @@
// automatically generated by stateify.
package sandbox

View file

@ -0,0 +1,195 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sandbox
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
// SnapshotType defines the type of snapshot.
type SnapshotType string
const (
// CheckpointRestore represents a full process state checkpoint and restore.
CheckpointRestore SnapshotType = "CheckpointRestore"
// FilesystemSnapshot represents a snapshot of the container's filesystems.
FilesystemSnapshot SnapshotType = "FilesystemSnapshot"
// RootfsTarSnapshot represents a tar file snapshot of rootfs changes.
RootfsTarSnapshot SnapshotType = "RootfsTarSnapshot"
)
// SnapshotID defines the type for snapshot IDs.
type SnapshotID string
// SnapshotMetadata stores the metadata of a snapshot.
type SnapshotMetadata struct {
Type SnapshotType `json:"type"`
CreatedAt string `json:"created_at"`
}
// Snapshot groups SnapshotID and SnapshotStorage together.
type Snapshot struct {
ID SnapshotID
Storage SnapshotStorage
}
// Asset defines the type for snapshot asset names.
type Asset string
const (
// MetadataAsset is the name of the metadata file.
MetadataAsset Asset = "metadata.json"
// RootfsAsset is the name of the rootfs tarball (if using RootfsTarSnapshot).
RootfsAsset Asset = "rootfs.tar"
// CheckpointAsset is the main checkpoint state file.
CheckpointAsset Asset = "checkpoint.img"
// PagesAsset is the memory pages file.
PagesAsset Asset = "pages.img"
// PagesMetaAsset is the memory pages metadata file.
PagesMetaAsset Asset = "pages_meta.img"
)
// SnapshotStorage defines a pluggable storage interface for snapshots.
type SnapshotStorage interface {
// PutWriter returns a WriteCloser to write a file asset of a snapshot.
PutWriter(ctx context.Context, snapshotID SnapshotID, assetName Asset) (io.WriteCloser, error)
// GetReader returns a ReadCloser to read a file asset of a snapshot.
GetReader(ctx context.Context, snapshotID SnapshotID, assetName Asset) (io.ReadCloser, error)
// Delete deletes all assets associated with a snapshot ID.
Delete(ctx context.Context, snapshotID SnapshotID) error
// List returns all snapshot IDs known to this storage.
List(ctx context.Context) ([]SnapshotID, error)
// Lookup verifies that the snapshot ID exists in this storage and returns a Snapshot.
Lookup(ctx context.Context, snapshotID SnapshotID) (*Snapshot, error)
}
// ErrSnapshotNotFound is returned when the snapshot ID is not found.
var ErrSnapshotNotFound = errors.New("snapshot not found")
// FilesystemStorage implements SnapshotStorage using a local directory.
type FilesystemStorage struct {
rootDir string
}
// NewFilesystemStorage creates a new FilesystemStorage at the given root directory.
// The root directory must already exist.
func NewFilesystemStorage(rootDir string) (*FilesystemStorage, error) {
fi, err := os.Stat(rootDir)
if err != nil {
return nil, err
}
if !fi.IsDir() {
return nil, fmt.Errorf("root directory %q is not a directory", rootDir)
}
return &FilesystemStorage{rootDir: rootDir}, nil
}
func sanitizeName(s string) (string, error) {
base := filepath.Base(s)
if base == "." || base == ".." || base == "/" || base == "\\" {
return "", fmt.Errorf("invalid name: %q", s)
}
return base, nil
}
// PutWriter returns a WriteCloser to write a file asset of a snapshot.
func (f *FilesystemStorage) PutWriter(ctx context.Context, snapshotID SnapshotID, assetName Asset) (io.WriteCloser, error) {
safeID, err := sanitizeName(string(snapshotID))
if err != nil {
return nil, fmt.Errorf("invalid snapshot ID: %w", err)
}
safeAsset, err := sanitizeName(string(assetName))
if err != nil {
return nil, fmt.Errorf("invalid asset name: %w", err)
}
path := filepath.Join(f.rootDir, safeID, safeAsset)
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return nil, err
}
return os.Create(path)
}
// GetReader returns a ReadCloser to read a file asset of a snapshot.
func (f *FilesystemStorage) GetReader(ctx context.Context, snapshotID SnapshotID, assetName Asset) (io.ReadCloser, error) {
safeID, err := sanitizeName(string(snapshotID))
if err != nil {
return nil, fmt.Errorf("invalid snapshot ID: %w", err)
}
safeAsset, err := sanitizeName(string(assetName))
if err != nil {
return nil, fmt.Errorf("invalid asset name: %w", err)
}
path := filepath.Join(f.rootDir, safeID, safeAsset)
return os.Open(path)
}
// Delete deletes all assets associated with a snapshot ID.
func (f *FilesystemStorage) Delete(ctx context.Context, snapshotID SnapshotID) error {
safeID, err := sanitizeName(string(snapshotID))
if err != nil {
return fmt.Errorf("invalid snapshot ID: %w", err)
}
path := filepath.Join(f.rootDir, safeID)
return os.RemoveAll(path)
}
// List returns all snapshot IDs known to this storage.
func (f *FilesystemStorage) List(ctx context.Context) ([]SnapshotID, error) {
entries, err := os.ReadDir(f.rootDir)
if err != nil {
return nil, err
}
ids := make([]SnapshotID, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
ids = append(ids, SnapshotID(entry.Name()))
}
}
return ids, nil
}
// Lookup returns a Snapshot by a given snapshot ID.
func (f *FilesystemStorage) Lookup(ctx context.Context, snapshotID SnapshotID) (*Snapshot, error) {
safeID, err := sanitizeName(string(snapshotID))
if err != nil {
return nil, fmt.Errorf("invalid snapshot ID: %w", err)
}
dir := filepath.Join(f.rootDir, safeID)
fi, err := os.Stat(dir)
if err != nil {
if os.IsNotExist(err) {
return nil, ErrSnapshotNotFound
}
return nil, err
}
if !fi.IsDir() {
return nil, fmt.Errorf("snapshot path %q is not a directory", dir)
}
return &Snapshot{
ID: SnapshotID(safeID),
Storage: f,
}, nil
}