Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix a read/write data race in the cache. #82

Merged
merged 2 commits into from
Sep 6, 2022
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
GO_CMD := go
GO_BUILD := $(GO_CMD) build
GO_TEST := $(GO_CMD) test -v -cover
GO_TEST := $(GO_CMD) test -race -v -cover

GO_LINT := golint -set_exit_status
GO_FMT := gofmt
Expand Down
6 changes: 3 additions & 3 deletions pkg/cdi/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ func (w *watch) setup(dirs []string, dirErrors map[string]error) {

// Start watching Spec directories for relevant changes.
func (w *watch) start(m *sync.Mutex, refresh func() error, dirErrors map[string]error) {
go w.watch(m, refresh, dirErrors)
go w.watch(w.watcher, m, refresh, dirErrors)
}

// Stop watching directories.
Expand All @@ -460,8 +460,8 @@ func (w *watch) stop() {
}

// Watch Spec directory changes, triggering a refresh if necessary.
func (w *watch) watch(m *sync.Mutex, refresh func() error, dirErrors map[string]error) {
watch := w.watcher
func (w *watch) watch(fsw *fsnotify.Watcher, m *sync.Mutex, refresh func() error, dirErrors map[string]error) {
watch := fsw
if watch == nil {
return
}
Expand Down
183 changes: 183 additions & 0 deletions pkg/cdi/regressions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package cdi

import (
"fmt"
"os"
"path/filepath"
"testing"

oci "github.com/opencontainers/runtime-spec/specs-go"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCDIInjectionRace(t *testing.T) {
// This is a gutted version of a containerd test case which triggered
// read/write data race in the Cache.

for _, test := range []struct {
description string
cdiSpecFiles []string
annotations map[string]string
expectError bool
expectDevices []oci.LinuxDevice
expectEnv []string
}{
{description: "expect no CDI error for nil annotations"},
{description: "expect no CDI error for empty annotations",
annotations: map[string]string{},
},
{description: "expect CDI error for invalid CDI device reference in annotations",
annotations: map[string]string{
AnnotationPrefix + "devices": "foobar",
},
expectError: true,
},
{description: "expect CDI error for unresolvable devices",
annotations: map[string]string{
AnnotationPrefix + "vendor1_devices": "vendor1.com/device=no-such-dev",
},
expectError: true,
},
{description: "expect properly injected resolvable CDI devices",
cdiSpecFiles: []string{
`
cdiVersion: "0.2.0"
kind: "vendor1.com/device"
devices:
- name: foo
containerEdits:
deviceNodes:
- path: /dev/loop8
type: b
major: 7
minor: 8
env:
- FOO=injected
containerEdits:
env:
- "VENDOR1=present"
`,
`
cdiVersion: "0.2.0"
kind: "vendor2.com/device"
devices:
- name: bar
containerEdits:
deviceNodes:
- path: /dev/loop9
type: b
major: 7
minor: 9
env:
- BAR=injected
containerEdits:
env:
- "VENDOR2=present"
`,
},
annotations: map[string]string{
AnnotationPrefix + "vendor1_devices": "vendor1.com/device=foo",
AnnotationPrefix + "vendor2_devices": "vendor2.com/device=bar",
},
expectDevices: []oci.LinuxDevice{
{
Path: "/dev/loop8",
Type: "b",
Major: 7,
Minor: 8,
},
{
Path: "/dev/loop9",
Type: "b",
Major: 7,
Minor: 9,
},
},
expectEnv: []string{
"FOO=injected",
"VENDOR1=present",
"BAR=injected",
"VENDOR2=present",
},
},
} {
t.Run(test.description, func(t *testing.T) {
var (
err error
spec = &oci.Spec{}
)

cdiDir, err := writeFilesToTempDir("containerd-test-CDI-injections-", test.cdiSpecFiles)
if cdiDir != "" {
defer os.RemoveAll(cdiDir)
}
require.NoError(t, err)

injectFun := withCDI(t, test.annotations, []string{cdiDir})
err = injectFun(spec)
assert.Equal(t, test.expectError, err != nil)

if err != nil {
if test.expectEnv != nil {
for _, expectedEnv := range test.expectEnv {
assert.Contains(t, spec.Process.Env, expectedEnv)
}
}
if test.expectDevices != nil {
for _, expectedDev := range test.expectDevices {
assert.Contains(t, spec.Linux.Devices, expectedDev)
}
}
}
})
}
}

type specOpts func(*oci.Spec) error

// withCDI (WithCDI) SpecOpt adopted from containerd.
func withCDI(t *testing.T, annotations map[string]string, cdiSpecDirs []string) specOpts {
return func(s *oci.Spec) error {
_, cdiDevices, err := ParseAnnotations(annotations)
if err != nil {
return fmt.Errorf("failed to parse CDI device annotations: %w", err)
}
if cdiDevices == nil {
return nil
}

registry := GetRegistry(WithSpecDirs(cdiSpecDirs...))
if err = registry.Refresh(); err != nil {
t.Logf("CDI registry refresh failed: %v", err)
}

if _, err := registry.InjectDevices(s, cdiDevices...); err != nil {
return fmt.Errorf("CDI device injection failed: %w", err)
}

return nil
}
}

func writeFilesToTempDir(tmpDirPattern string, content []string) (string, error) {
if len(content) == 0 {
return "", nil
}

dir, err := os.MkdirTemp("", tmpDirPattern)
if err != nil {
return "", err
}

for idx, data := range content {
file := filepath.Join(dir, fmt.Sprintf("spec-%d.yaml", idx))
err := os.WriteFile(file, []byte(data), 0644)
if err != nil {
return "", err
}
}

return dir, nil
}