Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
126 changes: 126 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
setup:
name: Setup
runs-on: ubuntu-latest
outputs:
kernel-version: ${{ steps.set-vars.outputs.kernel-version }}
version: ${{ steps.set-vars.outputs.version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
.github/.tool-versions

- name: Set variables
id: set-vars
run: |
kernel_version=$(grep -E '^kernel [0-9.]+$' .github/.tool-versions | sed -E 's/^kernel ([0-9.]+)$/\1/')
echo "kernel-version=${kernel_version}" >> $GITHUB_OUTPUT

version=${GITHUB_REF#refs/tags/v}
echo "version=${version}" >> $GITHUB_OUTPUT

build:
name: Build artifacts
needs: setup
runs-on: ubuntu-latest
timeout-minutes: 120
strategy:
fail-fast: true
matrix:
include:
- arch: x86_64
docker_arch: amd64

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0

- name: Build kernel
run: |
docker buildx bake kernel \
--set kernel.args.KERNEL_VERSION=${{ needs.setup.outputs.kernel-version }} \
--set kernel.args.KERNEL_ARCH=${{ matrix.arch }}

- name: Build host and guest binaries
run: |
docker buildx bake host-binaries guest-binaries \
--set '*.args.KERNEL_ARCH=${{ matrix.arch }}'

- name: Verify artifacts
run: |
echo "Verifying build artifacts:"
ls -lh _output/
file _output/nerdbox-kernel-${{ matrix.arch }}
file _output/nerdbox-initrd
file _output/containerd-shim-nerdbox-v1
file _output/libkrun.so

- name: Create release archive
run: |
ARCHIVE_DIR="nerdbox-${{ needs.setup.outputs.version }}-linux-${{ matrix.docker_arch }}"
mkdir -p "${ARCHIVE_DIR}"

cp _output/containerd-shim-nerdbox-v1 "${ARCHIVE_DIR}/"
cp _output/nerdbox-initrd "${ARCHIVE_DIR}/"
cp _output/nerdbox-kernel-${{ matrix.arch }} "${ARCHIVE_DIR}/"
cp _output/libkrun.so "${ARCHIVE_DIR}/libkrun-nerdbox.so"

tar -czf "${ARCHIVE_DIR}.tar.gz" "${ARCHIVE_DIR}/"

sha256sum "${ARCHIVE_DIR}.tar.gz" > "${ARCHIVE_DIR}.tar.gz.sha256sum"

- name: Upload artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: release-artifacts-linux-${{ matrix.docker_arch }}
path: |
nerdbox-*.tar.gz
nerdbox-*.tar.gz.sha256sum

release:
name: Create Release
needs: [setup, build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Download artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: release-artifacts
merge-multiple: true

- name: List release artifacts
run: ls -lh release-artifacts/

- name: Determine pre-release status
id: prerelease
run: |
VERSION="${{ needs.setup.outputs.version }}"
if [[ "${VERSION}" == *"beta"* ]] || [[ "${VERSION}" == *"rc"* ]] || [[ "${VERSION}" == *"alpha"* ]]; then
echo "prerelease=true" >> $GITHUB_OUTPUT
else
echo "prerelease=false" >> $GITHUB_OUTPUT
fi

- name: Create GitHub Release
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
with:
draft: false
prerelease: ${{ steps.prerelease.outputs.prerelease }}
generate_release_notes: true
make_latest: ${{ steps.prerelease.outputs.prerelease == 'false' }}
files: |
release-artifacts/*
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ tasks:
sh: go env GOARCH
sources:
- test/testbin/main.go
- vendor/github.com/dmcgowan/shimtest/testbin/testbin.go
- vendor/github.com/containerd/shimtest/testbin/testbin.go
generates:
- test/shim/testdata/testbin
- test/stress/testdata/testbin
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ require (
github.com/containerd/log v0.1.1-0.20260403072107-cb1839ebf76b
github.com/containerd/otelttrpc v0.1.0
github.com/containerd/plugin v1.1.0
github.com/containerd/shimtest v0.2.1
github.com/containerd/ttrpc v1.2.9-0.20260501231634-6c2eed2b612e
github.com/containerd/typeurl/v2 v2.3.0
github.com/dmcgowan/shimtest v0.1.8
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c
github.com/ebitengine/purego v0.10.1
github.com/insomniacslk/dhcp v0.0.0-20250919081422-f80a1952f48e
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0
github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U=
github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc=
github.com/containerd/shimtest v0.2.1 h1:v6DRcuU5TjwX9Qu+Q8suYvRp13UoJnDk5SPJKVIql3Q=
github.com/containerd/shimtest v0.2.1/go.mod h1:v9b7phlmKrfn9zKHqhDyoe0kv24mxDEYyJlXFcFhjnI=
github.com/containerd/ttrpc v1.2.9-0.20260501231634-6c2eed2b612e h1:uMP9FpdM40x+cvSyg6PiiINN9/b2908f8CsF/fZ438g=
github.com/containerd/ttrpc v1.2.9-0.20260501231634-6c2eed2b612e/go.mod h1:IvZPGIALrdh9ZNv7AvRrKHCfwVPPCueLO2yuwj3KIqE=
github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ=
Expand All @@ -47,8 +49,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dmcgowan/shimtest v0.1.8 h1:+HAqYd9EfClqrOHQ4vR3sTrW2DGpgb/vqdlUS7QRkZc=
github.com/dmcgowan/shimtest v0.1.8/go.mod h1:vV3SFMMBAY8xLOp8bO2xL3hFwlzCZWhVGTgKfV0L1EE=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
Expand Down
48 changes: 20 additions & 28 deletions internal/shim/task/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ func generateStreamID(prefix string) string {
return fmt.Sprintf("%s-%d-%s", prefix, time.Now().UnixNano(), base64.RawURLEncoding.EncodeToString(b[:]))
}

func (s *service) forwardIO(ctx context.Context, ss streamCreator, idPrefix string, sio stdio.Stdio) (stdio.Stdio, func(ctx context.Context) error, error) {
func (s *service) forwardIO(ctx context.Context, ss streamCreator, idPrefix string, sio stdio.Stdio) (stdio.Stdio, func(ctx context.Context) error, <-chan struct{}, func() error, error) {
pio := sio
if pio.IsNull() {
return pio, nil, nil
return pio, nil, nil, nil, nil
}
u, err := url.Parse(pio.Stdout)
if err != nil {
return stdio.Stdio{}, nil, fmt.Errorf("unable to parse stdout uri: %w", err)
return stdio.Stdio{}, nil, nil, nil, fmt.Errorf("unable to parse stdout uri: %w", err)
}
if u.Scheme == "" {
u.Scheme = defaultScheme
Expand All @@ -62,37 +62,37 @@ func (s *service) forwardIO(ctx context.Context, ss streamCreator, idPrefix stri
switch u.Scheme {
case "stream":
// Pass through
return pio, nil, nil
return pio, nil, nil, nil, nil
case "fifo", "pipe":
pio, streams, err = createStreams(ctx, ss, idPrefix, pio)
if err != nil {
return stdio.Stdio{}, nil, err
return stdio.Stdio{}, nil, nil, nil, err
}

//pio.io, err = runc.NewPipeIO(ioUID, ioGID, withConditionalIO(stdio))
case "file":
filePath := u.Path
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
return stdio.Stdio{}, nil, err
return stdio.Stdio{}, nil, nil, nil, err
}
var f *os.File
f, err = os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return stdio.Stdio{}, nil, err
return stdio.Stdio{}, nil, nil, nil, err
}
f.Close()
pio.Stdout = filePath
pio.Stderr = filePath
pio, streams, err = createStreams(ctx, ss, idPrefix, pio)
if err != nil {
return stdio.Stdio{}, nil, err
return stdio.Stdio{}, nil, nil, nil, err
}
default:
// TODO: Support "binary"
return stdio.Stdio{}, nil, fmt.Errorf("unsupported STDIO scheme %s: %w", u.Scheme, errdefs.ErrNotImplemented)
return stdio.Stdio{}, nil, nil, nil, fmt.Errorf("unsupported STDIO scheme %s: %w", u.Scheme, errdefs.ErrNotImplemented)
}
if err != nil {
return stdio.Stdio{}, nil, err
return stdio.Stdio{}, nil, nil, nil, err
}

defer func() {
Expand All @@ -105,25 +105,17 @@ func (s *service) forwardIO(ctx context.Context, ss streamCreator, idPrefix stri
}
}()
ioDone := make(chan struct{})
if err = copyStreams(ctx, streams, sio.Stdin, sio.Stdout, sio.Stderr, ioDone); err != nil {
return stdio.Stdio{}, nil, err
stdinEOF, err := copyStreams(ctx, streams, sio.Stdin, sio.Stdout, sio.Stderr, ioDone)
if err != nil {
return stdio.Stdio{}, nil, nil, nil, err
}
return pio, func(ctx context.Context) error {
// Wait for the copy goroutines to finish draining before closing
// the stream connections. Closing first causes goroutines that are
// mid-Read to see "use of closed network connection" and return
// early, dropping bytes still buffered in the kernel socket receive
// queue (the close-before-drain race).
//
// In normal operation ioDone always fires on its own: the container
// process exiting closes the runc pipe, vminitd's copy goroutine
// sees EOF and closes its vsock conn, which propagates EOF to the
// host copy goroutines here. ctx.Done() is only reached in
// exceptional cases (VM crash, vminitd hang, kernel wedge).
//
// Ensure the wait is always bounded: if the caller did not provide
// a deadline, apply a default so a wedged guest cannot pin cleanup
// indefinitely.
// ioDone is expected to already be closed by the time ioShutdown
// is called: the host Wait handler blocks until ioDone fires before
// returning to the caller, ensuring all buffered bytes have been
// drained to the FIFO before Delete is issued. This select is a
// safety net for error paths (VM crash, vminitd hang) where Wait
// may not have been called or ioDone may not have fired naturally.
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
Expand All @@ -141,7 +133,7 @@ func (s *service) forwardIO(ctx context.Context, ss streamCreator, idPrefix stri
}
}
return err
}, nil
}, ioDone, stdinEOF, nil
}

func createStreams(ctx context.Context, ss streamCreator, idPrefix string, io stdio.Stdio) (_ stdio.Stdio, conns [3]io.ReadWriteCloser, err error) {
Expand Down
74 changes: 74 additions & 0 deletions internal/shim/task/io_copystreams.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
Copyright The containerd 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

http://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 task

import (
"context"
"io"

"github.com/containerd/log"
)

// copyStdinUntilClose reads from f and writes raw bytes to sc until closeCh
// is closed (CloseIO) or f delivers EOF. On either exit it calls
// sc.CloseWrite() to send OP_SHUTDOWN(SEND) in-order on the vsock stdin
// stream, guaranteeing the guest sees EOF after all data already written —
// not via an out-of-band RPC that could race in-flight bytes.
func copyStdinUntilClose(ctx context.Context, sc interface {
io.Writer
CloseWrite() error
}, f io.Reader, buf []byte, closeCh <-chan struct{}) {
type readResult struct {
n int
err error
}
readCh := make(chan readResult, 1)
for {
go func() {
n, err := f.Read(buf)
readCh <- readResult{n, err}
}()
select {
case <-closeCh:
// CloseIO fired: drain the pending read then send in-band EOF.
res := <-readCh
if res.n > 0 {
if _, err := sc.Write(buf[:res.n]); err != nil {
log.G(ctx).WithError(err).Warn("error writing stdin on CloseIO")
}
}
if err := sc.CloseWrite(); err != nil {
log.G(ctx).WithError(err).Warn("error sending stdin EOF via CloseWrite")
}
return
case res := <-readCh:
if res.n > 0 {
if _, err := sc.Write(buf[:res.n]); err != nil {
log.G(ctx).WithError(err).Warn("error writing stdin")
return
}
}
if res.err != nil {
// Pipe/named-pipe EOF: client closed its write end.
if err := sc.CloseWrite(); err != nil {
log.G(ctx).WithError(err).Warn("error sending stdin EOF on pipe close")
}
return
}
}
}
}
Loading
Loading