1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
|
// Copyright (c) 2019, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.
package engine
import (
"context"
"encoding/json"
"fmt"
"net"
"net/rpc"
"os"
"syscall"
"github.com/sylabs/singularity/v4/internal/pkg/runtime/engine/config/starter"
"github.com/sylabs/singularity/v4/pkg/runtime/engine/config"
)
// Engine is the combination of an Operations and a config.Common. The singularity
// startup routines (internal/app/starter/*) can spawn a container process from this type.
type Engine struct {
Operations
*config.Common
}
// Operations is an interface describing necessary operations to launch
// a container process. Some of them may be called with elevated privilege
// or the potential to escalate privileges. Refer to an individual method
// documentation for a detailed description of the context in which it is called.
type Operations interface {
// Config returns a zero value of the current EngineConfig, which
// depends on the implementation, used to populate the Common struct.
//
// Since this method simply returns a zero value of the concrete
// EngineConfig, it does not matter whether or not there are any elevated
// privileges during this call.
Config() config.EngineConfig
// InitConfig stores the parsed config.Common inside the Operations
// implementation.
//
// Since this method simply stores config.Common, it does not matter
// whether or not there are any elevated privileges during this call.
InitConfig(*config.Common)
// PrepareConfig is called during stage1 to validate and prepare
// container configuration.
//
// No additional privileges can be gained as any of them are already
// dropped by the time PrepareConfig is called.
PrepareConfig(*starter.Config) error
// CreateContainer is called from master process to prepare container
// environment, e.g. perform mount operations, setup network, etc.
//
// Additional privileges required for setup may be gained when running
// in suid flow. However, when a user namespace is requested and it is not
// a hybrid workflow (e.g. fakeroot), then there is no privileged saved uid
// and thus no additional privileges can be gained.
CreateContainer(context.Context, int, net.Conn) error
// StartProcess is called during stage2 after RPC server finished
// environment preparation. This is the container process itself.
//
// No additional privileges can be gained during this call (unless container
// is executed as root intentionally) as starter will set uid/euid/suid
// to the targetUID (PrepareConfig will set it by calling starter.Config.SetTargetUID).
StartProcess(net.Conn) error
// PostStartProcess is called from master after successful
// execution of the container process.
//
// Additional privileges may be gained when running
// in suid flow. However, when a user namespace is requested and it is not
// a hybrid workflow (e.g. fakeroot), then there is no privileged saved uid
// and thus no additional privileges can be gained.
PostStartProcess(context.Context, int) error
// MonitorContainer is called from master once the container has
// been spawned. It will typically block until the container exists.
//
// Additional privileges may be gained when running
// in suid flow. However, when a user namespace is requested and it is not
// a hybrid workflow (e.g. fakeroot), then there is no privileged saved uid
// and thus no additional privileges can be gained.
MonitorContainer(int, chan os.Signal) (syscall.WaitStatus, error)
// CleanupContainer is called from master after the MonitorContainer returns.
// It is responsible for ensuring that the container has been properly torn down.
//
// Additional privileges may be gained when running
// in suid flow. However, when a user namespace is requested and it is not
// a hybrid workflow (e.g. fakeroot), then there is no privileged saved uid
// and thus no additional privileges can be gained.
CleanupContainer(context.Context, error, syscall.WaitStatus) error
// PostStartHost is called after the container process has been executed. It
// is run in the POST_START_HOST process forked from starter before
// namespace setup etc. and will perform any cleanup in the host mount
// namespace at time of CLI execution.
//
// No additional privileges can be gained during this call in the setuid
// flow as privileges are dropped permanently after forking in starter.
PostStartHost(context.Context) error
// CleanupHost is called on container exit or startup failure to perform any
// required cleanup in the host mount namespace at time of CLI execution.
//
// If container creation fails early, in STAGE 1, it will be called directly
// from STAGE 1. Otherwise it is run in the CLEANUP_HOST PROCESS, triggered
// by master, or the SIGKILL parent death signal.
//
// No additional privileges can be gained during this call in the setuid
// flow, as privileges are dropped permanently in both STAGE1 and
// CLEANUP_HOST after forking in starter.
CleanupHost(context.Context) error
}
// getName returns the engine name set in JSON []byte configuration.
func getName(b []byte) string {
engineName := struct {
EngineName string `json:"engineName"`
}{}
if err := json.Unmarshal(b, &engineName); err != nil {
return ""
}
return engineName.EngineName
}
// Get returns the engine described by the JSON []byte configuration.
func Get(b []byte) (*Engine, error) {
engineName := getName(b)
// ensure engine with given name is registered
eOp, ok := registeredOperations[engineName]
if !ok {
return nil, fmt.Errorf("engine %q is not found", engineName)
}
// create empty Engine object with properly initialized EngineConfig && Operations
e := &Engine{
Operations: eOp,
Common: &config.Common{
EngineConfig: eOp.Config(),
},
}
// parse received JSON configuration to specific EngineConfig
if err := json.Unmarshal(b, e.Common); err != nil {
return nil, fmt.Errorf("could not parse JSON configuration: %s", err)
}
e.InitConfig(e.Common)
return e, nil
}
var (
// registeredOperations contains a map relating an Engine name to a set
// of operations provided by an engine.
registeredOperations = make(map[string]Operations)
// registerEngineRPCMethods contains a map relating an Engine name to a set
// of RPC methods served by RPC server.
registeredRPCMethods = make(map[string]interface{})
)
// ServeRPCRequests serves runtime engine RPC requests with
// corresponding registered engine methods.
func ServeRPCRequests(e *Engine, conn net.Conn) {
methods, ok := registeredRPCMethods[e.EngineName]
if ok {
rpc.RegisterName(e.EngineName, methods)
rpc.ServeConn(conn)
}
}
// RegisterOperations registers engine operations for a runtime engine.
func RegisterOperations(name string, operations Operations) {
registeredOperations[name] = operations
}
// RegisterRPCMethods registers engine RPC methods served by RPC server.
func RegisterRPCMethods(name string, methods interface{}) {
registeredRPCMethods[name] = methods
}
|