Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/execd-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ jobs:
chmod +x tests/sigterm_forward.sh
./tests/sigterm_forward.sh

- name: Lifecycle bootstrap test
if: matrix.os == 'ubuntu-latest'
timeout-minutes: 2
working-directory: components/execd
run: bash tests/lifecycle.sh

- name: Smoke test bwrap (Docker image build + extraction)
if: matrix.os == 'ubuntu-latest'
shell: bash
Expand Down
88 changes: 87 additions & 1 deletion components/execd/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ _shutdown_children() {
if [ -n "${EXECD_PID:-}" ]; then
wait "$EXECD_PID" 2>/dev/null || true
fi
_cleanup_lifecycle_status
exit 0
}

Expand All @@ -49,6 +50,23 @@ is_truthy() {
esac
}

has_lifecycle_config() {
# Keep this in sync with pkg/lifecycle/config.go's transport env, explicit
# path env, and default persisted path.
[ -n "${OPEN_SANDBOX_LIFECYCLE:-}" ] \
|| { [ -n "${EXECD_LIFECYCLE_CONFIG:-}" ] && [ -f "$EXECD_LIFECYCLE_CONFIG" ]; } \
|| [ -f /var/execd/lifecycle.toml ]
}

_cleanup_lifecycle_status() {
if [ -n "${LIFECYCLE_STATUS_FILE:-}" ]; then
rm -f "$LIFECYCLE_STATUS_FILE"
LIFECYCLE_STATUS_FILE=""
fi
}

trap '_cleanup_lifecycle_status' EXIT

_sudo() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
Expand Down Expand Up @@ -349,9 +367,77 @@ if is_truthy "${EXECD_INIT:-}"; then
exec "$EXECD" --init -- "$@"
fi

"$EXECD" &
LIFECYCLE_STATUS_FILE=""
if has_lifecycle_config; then
if ! LIFECYCLE_STATUS_FILE="$(
mktemp "${TMPDIR:-/tmp}/execd-lifecycle.XXXXXX" 2>/dev/null \
|| mktemp /tmp/execd-lifecycle.XXXXXX 2>/dev/null
)"; then
echo "error: failed to create lifecycle startup status file" >&2
exit 1
fi
"$EXECD" --lifecycle-startup-status-file "$LIFECYCLE_STATUS_FILE" &
else
"$EXECD" &
fi
EXECD_PID=$!

# The same long-running execd starts serving HTTP, executes preStart, then
# reports the result through this private bootstrap synchronization file.
if [ -n "$LIFECYCLE_STATUS_FILE" ]; then
while [ ! -s "$LIFECYCLE_STATUS_FILE" ]; do
Comment thread
jianpingpei marked this conversation as resolved.
Outdated
_execd_state=""
if [ -r "/proc/$EXECD_PID/stat" ]; then
_execd_state="$(sed -e 's/^.*) //' -e 's/ .*$//' "/proc/$EXECD_PID/stat" 2>/dev/null || true)"
fi
if ! kill -0 "$EXECD_PID" 2>/dev/null || [ "$_execd_state" = "Z" ]; then
set +e
wait "$EXECD_PID"
_execd_status=$?
set -e
EXECD_PID=""
_cleanup_lifecycle_status
if [ "$_execd_status" -eq 0 ]; then
_execd_status=1
fi
exit "$_execd_status"
fi
# Execd enforces the configured hook timeout; this loop additionally
# exits as soon as the daemon dies before reporting a result.
sleep 0.1 2>/dev/null || sleep 1
done
IFS= read -r _prestart_status < "$LIFECYCLE_STATUS_FILE" || true
_cleanup_lifecycle_status
case "${_prestart_status:-}" in
"" | *[!0-9]*) _prestart_status=1 ;;
esac
if [ "$_prestart_status" -ne 0 ]; then
_forward_signal TERM "$EXECD_PID"
(
sleep 10 &
_sleep_pid=$!
trap 'kill "$_sleep_pid" 2>/dev/null || true; exit 0' TERM INT
wait "$_sleep_pid"
_forward_signal KILL "$EXECD_PID"
) &
_shutdown_watchdog=$!
set +e
wait "$EXECD_PID"
_execd_status=$?
kill "$_shutdown_watchdog" 2>/dev/null || true
wait "$_shutdown_watchdog" 2>/dev/null || true
set -e
EXECD_PID=""
echo "error: lifecycle preStart failed (status $_prestart_status, execd exit $_execd_status)" >&2
if [ "$_prestart_status" -le 0 ] || [ "$_prestart_status" -gt 255 ]; then
_prestart_status=1
fi
exit "$_prestart_status"
fi
unset _prestart_status _execd_status
fi

unset OPEN_SANDBOX_LIFECYCLE EXECD_LIFECYCLE_CONFIG
"$@" &
CMD_PID=$!

Expand Down
2 changes: 2 additions & 0 deletions components/execd/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ require (
k8s.io/client-go v0.34.2
)

require github.com/robfig/cron/v3 v3.0.1
Comment thread
jianpingpei marked this conversation as resolved.
Outdated

require (
filippo.io/edwards25519 v1.1.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
Expand Down
2 changes: 2 additions & 0 deletions components/execd/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
Expand Down
139 changes: 129 additions & 10 deletions components/execd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/alibaba/opensandbox/execd/pkg/ebpf"
"github.com/alibaba/opensandbox/execd/pkg/flag"
"github.com/alibaba/opensandbox/execd/pkg/isolation"
"github.com/alibaba/opensandbox/execd/pkg/lifecycle"
"github.com/alibaba/opensandbox/execd/pkg/log"
"github.com/alibaba/opensandbox/execd/pkg/runtime"
"github.com/alibaba/opensandbox/execd/pkg/telemetry"
Expand All @@ -48,6 +49,8 @@ const (
isolatedRunnerCloseRetryInterval = 100 * time.Millisecond
)

var errInitStartupShutdown = errors.New("init startup interrupted by shutdown")

type isolatedRunnerCloser interface {
Close() error
}
Expand All @@ -62,6 +65,7 @@ func run() int {
version.EchoVersion("OpenSandbox Execd")

flag.InitFlags()
log.Init(flag.ServerLogLevel)

// Load isolation config.
isoCfg, err := isolation.LoadConfig(flag.IsolationConfigPath)
Expand All @@ -78,6 +82,16 @@ func run() int {
return 1
}

// Materialize the internal environment transport before the HTTP server
// can launch user code, then remove it from execd's process environment.
lifecycleConfig, err := lifecycle.LoadConfig()
if err != nil {
log.Error("lifecycle: config: %v", err)
return 1
}
_ = os.Unsetenv(lifecycle.ConfigEnv)
_ = os.Unsetenv(lifecycle.ConfigPathEnv)

// Start the eBPF observation layer ([ebpf] enabled, OSEP-0018 §5).
// The stub build reports disabled; the execd-ebpf variant attaches the
// exec/connect/privilege hooks.
Expand All @@ -94,12 +108,17 @@ func run() int {
log.Info("isolation: available=%v isolator=%s version=%s",
isolationProbe.Available, isolationProbe.Isolator, isolationProbe.Version)

log.Init(flag.ServerLogLevel)

var startInitEntrypoint func([]string) error
var initStartupCtx context.Context
var stopInitStartupSignals context.CancelFunc
if flag.InitMode {
// Start after the startup probes (which run short-lived cmd.Run
// children) so the reaper is the only wait4 caller from here on.
runtime.StartInitMode(flag.Args())
initStartupCtx, stopInitStartupSignals = signal.NotifyContext(
context.Background(), os.Interrupt, syscall.SIGTERM,
)
startInitEntrypoint = runtime.PrepareInitMode()
defer stopInitStartupSignals()
}

ctrl := controller.InitCodeRunner()
Expand Down Expand Up @@ -150,30 +169,121 @@ func run() int {
}

engine := web.NewRouter(flag.ServerAccessToken)
if err := runHTTPServer(
engine,
startInitEntrypoint,
initStartupCtx,
stopInitStartupSignals,
lifecycleConfig,
); err != nil {
if errors.Is(err, errInitStartupShutdown) {
log.Info("init: shutdown requested before user entrypoint started")
return 0
}
log.Error("execd server stopped with error: %v", err)
return 1
}
return 0
}

func runHTTPServer(
engine http.Handler,
startInitEntrypoint func([]string) error,
initStartupCtx context.Context,
stopInitStartupSignals context.CancelFunc,
lifecycleConfig *lifecycle.Config,
) error {
addr := fmt.Sprintf(":%d", flag.ServerPort)
listener, err := net.Listen("tcp4", addr)
if err != nil {
log.Error("failed to listen on %s: %v", addr, err)
return 1
return fmt.Errorf("listen on %s: %w", addr, err)
}
log.Info("execd listening on %s (IPv4)", addr)
// In init mode SIGTERM belongs to the init lifecycle (forward + graceful
// shutdown with the entrypoint's exit status); only SIGINT cancels the
// HTTP server there.
ctxSignals := []os.Signal{os.Interrupt}
if !flag.InitMode {
if !flag.InitMode || len(flag.Args()) == 0 {
ctxSignals = append(ctxSignals, syscall.SIGTERM)
}
serverCtx, stopSignals := signal.NotifyContext(
context.Background(),
ctxSignals...,
)
defer stopSignals()
if err := serveHTTPUntilShutdown(serverCtx, listener, engine); err != nil {
log.Error("execd server stopped with error: %v", err)
return 1
var periodicManager *lifecycle.PeriodicManager
defer func() {
if periodicManager != nil {
periodicManager.Stop()
}
}()
startup := func() error {
preStartCtx := serverCtx
if flag.InitMode {
preStartCtx = initStartupCtx
if initStartupCtx.Err() != nil {
return errInitStartupShutdown
}
}
periodicManager, err = startLifecycle(
preStartCtx,
lifecycleConfig,
flag.LifecycleStartupStatusFile,
)
if err != nil {
if flag.InitMode && initStartupCtx.Err() != nil {
return errInitStartupShutdown
}
return err
}
if flag.InitMode {
stopInitStartupSignals()
if err := startInitEntrypoint(flag.Args()); err != nil {
return err
}
}
return nil
}
return 0
return serveHTTPUntilShutdown(serverCtx, listener, engine, startup)
}

func startLifecycle(
ctx context.Context,
cfg *lifecycle.Config,
statusFile string,
) (*lifecycle.PeriodicManager, error) {
if cfg != nil && cfg.PreStart != nil {
if err := lifecycle.RunPreStart(ctx, cfg); err != nil {
reportErr := writeLifecycleStartupStatus(statusFile, 1)
return nil, errors.Join(
fmt.Errorf("lifecycle preStart: %w", err),
reportErr,
)
}
}

periodicManager, err := lifecycle.StartPeriodic(cfg)
if err != nil {
log.Error("lifecycle: periodic hooks disabled: %v", err)
periodicManager = nil
}
if err := writeLifecycleStartupStatus(statusFile, 0); err != nil {
if periodicManager != nil {
periodicManager.Stop()
}
return nil, err
}
return periodicManager, nil
}

func writeLifecycleStartupStatus(path string, status int) error {
if path == "" {
return nil
}
if err := os.WriteFile(path, []byte(fmt.Sprintf("%d\n", status)), 0o600); err != nil {
return fmt.Errorf("write lifecycle startup status: %w", err)
}
return nil
}

func closeIsolatedRunnerWithRetry(
Expand Down Expand Up @@ -230,12 +340,21 @@ func serveHTTPUntilShutdown(
ctx context.Context,
listener net.Listener,
handler http.Handler,
startup func() error,
) error {
server := &http.Server{Handler: handler}
serveDone := make(chan error, 1)
go func() {
serveDone <- server.Serve(listener)
}()
if err := startup(); err != nil {
closeErr := server.Close()
serveErr := <-serveDone
if errors.Is(serveErr, http.ErrServerClosed) {
serveErr = nil
}
return errors.Join(err, closeErr, serveErr)
}

select {
case err := <-serveDone:
Expand Down
Loading
Loading