Skip to content

Commit 678393e

Browse files
authored
Merge pull request containerd#3364 from apostasie/hardening-filestore
Add a generic abstraction for filesystem based stores
2 parents 251fa8b + bf89c08 commit 678393e

33 files changed

+1685
-999
lines changed

cmd/nerdctl/namespace/namespace.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package namespace
1818

1919
import (
2020
"fmt"
21-
"os"
2221
"sort"
2322
"strings"
2423
"text/tabwriter"
@@ -120,13 +119,10 @@ func namespaceLsAction(cmd *cobra.Command, args []string) error {
120119
if err != nil {
121120
log.L.Warn(err)
122121
} else {
123-
volEnts, err := volStore.List(false)
122+
numVolumes, err = volStore.Count()
124123
if err != nil {
125-
if !os.IsNotExist(err) {
126-
log.L.Warn(err)
127-
}
124+
log.L.Warn(err)
128125
}
129-
numVolumes = len(volEnts)
130126
}
131127

132128
labels, err := client.NamespaceService().Labels(ctx, ns)

pkg/cmd/compose/compose.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,8 @@ func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, op
7777
if err != nil {
7878
return nil, err
7979
}
80-
options.VolumeExists = func(volName string) (bool, error) {
81-
_, volGetErr := volStore.Get(volName, false)
82-
if volGetErr == nil {
83-
return true, nil
84-
} else if errors.Is(volGetErr, errdefs.ErrNotFound) {
85-
return false, nil
86-
}
87-
return false, volGetErr
88-
}
80+
// FIXME: this is racy. See note in up_volume.go
81+
options.VolumeExists = volStore.Exists
8982

9083
options.ImageExists = func(ctx context.Context, rawRef string) (bool, error) {
9184
refNamed, err := referenceutil.ParseAny(rawRef)

pkg/cmd/container/create.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,14 @@ import (
5959
"github.com/containerd/nerdctl/v2/pkg/platformutil"
6060
"github.com/containerd/nerdctl/v2/pkg/referenceutil"
6161
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
62+
"github.com/containerd/nerdctl/v2/pkg/store"
6263
"github.com/containerd/nerdctl/v2/pkg/strutil"
6364
)
6465

6566
// Create will create a container.
6667
func Create(ctx context.Context, client *containerd.Client, args []string, netManager containerutil.NetworkOptionsManager, options types.ContainerCreateOptions) (containerd.Container, func(), error) {
67-
// Acquire an exclusive lock on the volume store until we are done to avoid being raced by other volume operations
68+
// Acquire an exclusive lock on the volume store until we are done to avoid being raced by any other
69+
// volume operations (or any other operation involving volume manipulation)
6870
volStore, err := volume.Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address)
6971
if err != nil {
7072
return nil, nil, err
@@ -73,7 +75,7 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
7375
if err != nil {
7476
return nil, nil, err
7577
}
76-
defer volStore.Unlock()
78+
defer volStore.Release()
7779

7880
// simulate the behavior of double dash
7981
newArg := []string{}
@@ -840,7 +842,8 @@ func generateGcFunc(ctx context.Context, container containerd.Container, ns, id,
840842
if containerNameStore, errE = namestore.New(dataStore, ns); errE != nil {
841843
log.G(ctx).WithError(errE).Warnf("failed to instantiate container name store during cleanup for container %q", id)
842844
}
843-
if errE = containerNameStore.Release(name, id); errE != nil {
845+
// Double-releasing may happen with containers started with --rm, so, ignore NotFound errors
846+
if errE := containerNameStore.Release(name, id); errE != nil && !errors.Is(errE, store.ErrNotFound) {
844847
log.G(ctx).WithError(errE).Warnf("failed to release container name store for container %q (%s)", name, id)
845848
}
846849
}

pkg/cmd/container/remove.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ import (
3232

3333
"github.com/containerd/nerdctl/v2/pkg/api/types"
3434
"github.com/containerd/nerdctl/v2/pkg/clientutil"
35-
"github.com/containerd/nerdctl/v2/pkg/cmd/volume"
3635
"github.com/containerd/nerdctl/v2/pkg/containerutil"
3736
"github.com/containerd/nerdctl/v2/pkg/dnsutil/hostsstore"
3837
"github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
3938
"github.com/containerd/nerdctl/v2/pkg/ipcutil"
4039
"github.com/containerd/nerdctl/v2/pkg/labels"
40+
"github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore"
4141
"github.com/containerd/nerdctl/v2/pkg/namestore"
42+
"github.com/containerd/nerdctl/v2/pkg/store"
4243
)
4344

4445
var _ error = ErrContainerStatus{}
@@ -128,18 +129,10 @@ func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions
128129
return err
129130
}
130131
// Get volume store
131-
volStore, err := volume.Store(globalOptions.Namespace, globalOptions.DataRoot, globalOptions.Address)
132+
volStore, err := volumestore.New(dataStore, globalOptions.Namespace)
132133
if err != nil {
133134
return err
134135
}
135-
// Note: technically, it is not strictly necessary to acquire an exclusive lock on the volume store here.
136-
// Worst case scenario, we would fail removing anonymous volumes later on, which is a soft error, and which would
137-
// only happen if we concurrently tried to remove the same container.
138-
err = volStore.Lock()
139-
if err != nil {
140-
return err
141-
}
142-
defer volStore.Unlock()
143136
// Decode IPC
144137
ipc, err := ipcutil.DecodeIPCLabel(containerLabels[labels.IPC])
145138
if err != nil {
@@ -212,24 +205,34 @@ func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions
212205

213206
// Enforce release name here in case the poststop hook name release fails - soft failure
214207
if name != "" {
215-
if err = nameStore.Release(name, id); err != nil {
208+
// Double-releasing may happen with containers started with --rm, so, ignore NotFound errors
209+
if err := nameStore.Release(name, id); err != nil && !errors.Is(err, store.ErrNotFound) {
216210
log.G(ctx).WithError(err).Warnf("failed to release container name %s", name)
217211
}
218212
}
219213

220-
// De-allocate hosts file - soft failure
221-
if err = hostsstore.DeallocHostsFile(dataStore, containerNamespace, id); err != nil {
214+
hs, err := hostsstore.New(dataStore, containerNamespace)
215+
if err != nil {
216+
log.G(ctx).WithError(err).Warnf("failed to instantiate hostsstore for %q", containerNamespace)
217+
} else if err = hs.DeallocHostsFile(id); err != nil {
218+
// De-allocate hosts file - soft failure
222219
log.G(ctx).WithError(err).Warnf("failed to remove hosts file for container %q", id)
223220
}
224221

225222
// Volume removal is not handled by the poststop hook lifecycle because it depends on removeAnonVolumes option
223+
// Note that the anonymous volume list has been obtained earlier, without locking the volume store.
224+
// Technically, a concurrent operation MAY have deleted these anonymous volumes already at this point, which
225+
// would make this operation here "soft fail".
226+
// This is not a problem per-se, though we will output a warning in that case.
226227
if anonVolumesJSON, ok := containerLabels[labels.AnonymousVolumes]; ok && removeAnonVolumes {
227228
var anonVolumes []string
228229
if err = json.Unmarshal([]byte(anonVolumesJSON), &anonVolumes); err != nil {
229230
log.G(ctx).WithError(err).Warnf("failed to unmarshall anonvolume information for container %q", id)
230231
} else {
231232
var errs []error
232-
_, errs, err = volStore.Remove(anonVolumes)
233+
_, errs, err = volStore.Remove(func() ([]string, []error, error) {
234+
return anonVolumes, nil, nil
235+
})
233236
if err != nil || len(errs) > 0 {
234237
log.G(ctx).WithError(err).Warnf("failed to remove anonymous volumes %v", anonVolumes)
235238
}

pkg/cmd/container/rename.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func Rename(ctx context.Context, client *containerd.Client, containerID, newCont
4444
if err != nil {
4545
return err
4646
}
47-
hostst, err := hostsstore.NewStore(dataStore)
47+
hostst, err := hostsstore.New(dataStore, options.GOptions.Namespace)
4848
if err != nil {
4949
return err
5050
}
@@ -84,7 +84,7 @@ func renameContainer(ctx context.Context, container containerd.Container, newNam
8484
labels.Name: name,
8585
}
8686
namst.Rename(newName, id, name)
87-
hostst.Update(ns, id, name)
87+
hostst.Update(id, name)
8888
container.SetLabels(ctx, lbls)
8989
}
9090
}()
@@ -93,7 +93,7 @@ func renameContainer(ctx context.Context, container containerd.Container, newNam
9393
return err
9494
}
9595
if runtime.GOOS == "linux" {
96-
if err = hostst.Update(ns, id, newName); err != nil {
96+
if err = hostst.Update(id, newName); err != nil {
9797
log.G(ctx).WithError(err).Warn("failed to update host networking definitions " +
9898
"- if your container is using network 'none', this is expected - otherwise, please report this as a bug")
9999
}

pkg/cmd/container/run_mount.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm
258258

259259
log.G(ctx).Debugf("creating anonymous volume %q, for \"VOLUME %s\"",
260260
anonVolName, imgVolRaw)
261-
anonVol, err := volStore.Create(anonVolName, []string{})
261+
anonVol, err := volStore.CreateWithoutLock(anonVolName, []string{})
262262
if err != nil {
263263
return nil, nil, nil, err
264264
}

pkg/cmd/volume/prune.go

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ package volume
1919
import (
2020
"context"
2121
"fmt"
22+
"strings"
2223

2324
containerd "github.com/containerd/containerd/v2/client"
2425

2526
"github.com/containerd/nerdctl/v2/pkg/api/types"
27+
"github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
2628
"github.com/containerd/nerdctl/v2/pkg/labels"
2729
)
2830

@@ -33,60 +35,50 @@ func Prune(ctx context.Context, client *containerd.Client, options types.VolumeP
3335
if err != nil {
3436
return err
3537
}
36-
err = volStore.Lock()
37-
if err != nil {
38-
return err
39-
}
40-
defer volStore.Unlock()
4138

42-
// Get containers and see which volumes are used
43-
containers, err := client.Containers(ctx)
44-
if err != nil {
45-
return err
46-
}
39+
var toRemove []string // nolint: prealloc
4740

48-
usedVolumesList, err := usedVolumes(ctx, containers)
49-
if err != nil {
50-
return err
51-
}
52-
var removeNames []string // nolint: prealloc
53-
54-
// Get the list of known volumes from the store
55-
volumes, err := volStore.List(false)
56-
if err != nil {
57-
return err
58-
}
41+
err = volStore.Prune(func(volumes []*native.Volume) ([]string, error) {
42+
// Get containers and see which volumes are used
43+
containers, err := client.Containers(ctx)
44+
if err != nil {
45+
return nil, err
46+
}
5947

60-
// Iterate through the known volumes, making sure we do not remove in-use volumes
61-
// but capture as well anon volumes (if --all was passed)
62-
for _, volume := range volumes {
63-
if _, ok := usedVolumesList[volume.Name]; ok {
64-
continue
48+
usedVolumesList, err := usedVolumes(ctx, containers)
49+
if err != nil {
50+
return nil, err
6551
}
66-
if !options.All {
67-
if volume.Labels == nil {
52+
53+
for _, volume := range volumes {
54+
if _, ok := usedVolumesList[volume.Name]; ok {
6855
continue
6956
}
70-
val, ok := (*volume.Labels)[labels.AnonymousVolumes]
71-
//skip the named volume and only remove the anonymous volume
72-
if !ok || val != "" {
73-
continue
57+
if !options.All {
58+
if volume.Labels == nil {
59+
continue
60+
}
61+
val, ok := (*volume.Labels)[labels.AnonymousVolumes]
62+
// skip the named volume and only remove the anonymous volume
63+
if !ok || val != "" {
64+
continue
65+
}
7466
}
67+
toRemove = append(toRemove, volume.Name)
7568
}
76-
removeNames = append(removeNames, volume.Name)
77-
}
7869

79-
// Remove the volumes from that list
80-
removedNames, _, err := volStore.Remove(removeNames)
70+
return toRemove, nil
71+
})
72+
8173
if err != nil {
8274
return err
8375
}
84-
if len(removedNames) > 0 {
76+
77+
if len(toRemove) > 0 {
8578
fmt.Fprintln(options.Stdout, "Deleted Volumes:")
86-
for _, name := range removedNames {
87-
fmt.Fprintln(options.Stdout, name)
88-
}
79+
fmt.Fprintln(options.Stdout, strings.Join(toRemove, "\n"))
8980
fmt.Fprintln(options.Stdout, "")
9081
}
82+
9183
return nil
9284
}

pkg/cmd/volume/rm.go

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,45 +33,38 @@ import (
3333
)
3434

3535
func Remove(ctx context.Context, client *containerd.Client, volumes []string, options types.VolumeRemoveOptions) error {
36-
// Get the volume store and lock it until we are done.
37-
// This will prevent racing new containers from being created or removed until we are done with the cleanup of volumes
3836
volStore, err := Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address)
3937
if err != nil {
4038
return err
4139
}
42-
err = volStore.Lock()
43-
if err != nil {
44-
return err
45-
}
46-
defer volStore.Unlock()
4740

48-
// Get containers and see which volumes are used
4941
containers, err := client.Containers(ctx)
5042
if err != nil {
5143
return err
5244
}
5345

54-
usedVolumesList, err := usedVolumes(ctx, containers)
55-
if err != nil {
56-
return err
57-
}
58-
59-
volumeNames := []string{}
60-
cannotRemove := []error{}
46+
// Note: to avoid racy behavior, this is called by volStore.Remove *inside a lock*
47+
removableVolumes := func() (volumeNames []string, cannotRemove []error, err error) {
48+
usedVolumesList, err := usedVolumes(ctx, containers)
49+
if err != nil {
50+
return nil, nil, err
51+
}
6152

62-
for _, name := range volumes {
63-
if _, ok := usedVolumesList[name]; ok {
64-
cannotRemove = append(cannotRemove, fmt.Errorf("volume %q is in use (%w)", name, errdefs.ErrFailedPrecondition))
65-
continue
53+
for _, name := range volumes {
54+
if _, ok := usedVolumesList[name]; ok {
55+
cannotRemove = append(cannotRemove, fmt.Errorf("volume %q is in use (%w)", name, errdefs.ErrFailedPrecondition))
56+
continue
57+
}
58+
volumeNames = append(volumeNames, name)
6659
}
67-
volumeNames = append(volumeNames, name)
60+
61+
return volumeNames, cannotRemove, nil
6862
}
69-
// if err is set, this is a hard filesystem error
70-
removedNames, warns, err := volStore.Remove(volumeNames)
63+
64+
removedNames, cannotRemove, err := volStore.Remove(removableVolumes)
7165
if err != nil {
7266
return err
7367
}
74-
cannotRemove = append(cannotRemove, warns...)
7568
// Otherwise, output on stdout whatever was successful
7669
for _, name := range removedNames {
7770
fmt.Fprintln(options.Stdout, name)

pkg/composer/down.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ func (c *Composer) downVolume(ctx context.Context, shortName string) error {
128128
}
129129
// shortName is like "db_data", fullName is like "compose-wordpress_db_data"
130130
fullName := vol.Name
131+
// FIXME: this is racy. See note in up_volume.go
131132
volExists, err := c.VolumeExists(fullName)
132133
if err != nil {
133134
return err

pkg/composer/up_volume.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ func (c *Composer) upVolume(ctx context.Context, shortName string) error {
4242

4343
// shortName is like "db_data", fullName is like "compose-wordpress_db_data"
4444
fullName := vol.Name
45-
// FIXME: this is racy. By the time we get below to creating the volume, there is no guarantee that things are still fine
46-
// Furthermore, volStore.Get no longer errors if the volume already exists (docker behavior), so, the purpose of this
47-
// call needs to be assessed (it might still error if the name is malformed, or if there is a filesystem error)
45+
// FIXME: this is racy. By the time we get below to creating the volume, there is no guarantee that things are still fine.
46+
// Furthermore, by the time we are done creating all the volumes, they may very well have been destroyed.
47+
// This cannot be fixed without getting rid of the whole "shell-out" approach entirely.
4848
volExists, err := c.VolumeExists(fullName)
4949
if err != nil {
5050
return err

0 commit comments

Comments
 (0)