Skip to content

Commit 0074265

Browse files
authored
Merge pull request #2980 from alvaroaleman/fictoctou
🐛 Fakeclient: Fix TOCTOU races
2 parents 685db56 + e643571 commit 0074265

File tree

2 files changed

+309
-11
lines changed

2 files changed

+309
-11
lines changed

pkg/client/fake/client.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,21 @@ type versionedTracker struct {
6969
}
7070

7171
type fakeClient struct {
72-
tracker versionedTracker
73-
scheme *runtime.Scheme
72+
// trackerWriteLock must be acquired before writing to
73+
// the tracker or performing reads that affect a following
74+
// write.
75+
trackerWriteLock sync.Mutex
76+
tracker versionedTracker
77+
78+
schemeWriteLock sync.Mutex
79+
scheme *runtime.Scheme
80+
7481
restMapper meta.RESTMapper
7582
withStatusSubresource sets.Set[schema.GroupVersionKind]
7683

7784
// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
7885
// The inner map maps from index name to IndexerFunc.
7986
indexes map[schema.GroupVersionKind]map[string]client.IndexerFunc
80-
81-
schemeWriteLock sync.Mutex
8287
}
8388

8489
var _ client.WithWatch = &fakeClient{}
@@ -468,6 +473,11 @@ func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runt
468473
switch {
469474
case allowsUnconditionalUpdate(gvk):
470475
accessor.SetResourceVersion(oldAccessor.GetResourceVersion())
476+
// This is needed because if the patch explicitly sets the RV to null, the client-go reaction we use
477+
// to apply it and whose output we process here will have it unset. It is not clear why the Kubernetes
478+
// apiserver accepts such a patch, but it does so we just copy that behavior.
479+
// Kubernetes apiserver behavior can be checked like this:
480+
// `kubectl patch configmap foo --patch '{"metadata":{"annotations":{"foo":"bar"},"resourceVersion":null}}' -v=9`
471481
case bytes.
472482
Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Patch")):
473483
// We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change
@@ -733,6 +743,8 @@ func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...clie
733743
accessor.SetDeletionTimestamp(nil)
734744
}
735745

746+
c.trackerWriteLock.Lock()
747+
defer c.trackerWriteLock.Unlock()
736748
return c.tracker.Create(gvr, obj, accessor.GetNamespace())
737749
}
738750

@@ -754,6 +766,8 @@ func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...clie
754766
}
755767
}
756768

769+
c.trackerWriteLock.Lock()
770+
defer c.trackerWriteLock.Unlock()
757771
// Check the ResourceVersion if that Precondition was specified.
758772
if delOptions.Preconditions != nil && delOptions.Preconditions.ResourceVersion != nil {
759773
name := accessor.GetName()
@@ -776,7 +790,7 @@ func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...clie
776790
}
777791
}
778792

779-
return c.deleteObject(gvr, accessor)
793+
return c.deleteObjectLocked(gvr, accessor)
780794
}
781795

782796
func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
@@ -794,6 +808,9 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ..
794808
}
795809
}
796810

811+
c.trackerWriteLock.Lock()
812+
defer c.trackerWriteLock.Unlock()
813+
797814
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
798815
o, err := c.tracker.List(gvr, gvk, dcOptions.Namespace)
799816
if err != nil {
@@ -813,7 +830,7 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ..
813830
if err != nil {
814831
return err
815832
}
816-
err = c.deleteObject(gvr, accessor)
833+
err = c.deleteObjectLocked(gvr, accessor)
817834
if err != nil {
818835
return err
819836
}
@@ -843,6 +860,9 @@ func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.Upd
843860
if err != nil {
844861
return err
845862
}
863+
864+
c.trackerWriteLock.Lock()
865+
defer c.trackerWriteLock.Unlock()
846866
return c.tracker.update(gvr, obj, accessor.GetNamespace(), isStatus, false, *updateOptions.AsUpdateOptions())
847867
}
848868

@@ -878,6 +898,8 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
878898
return err
879899
}
880900

901+
c.trackerWriteLock.Lock()
902+
defer c.trackerWriteLock.Unlock()
881903
oldObj, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName())
882904
if err != nil {
883905
return err
@@ -1086,7 +1108,7 @@ func (c *fakeClient) SubResource(subResource string) client.SubResourceClient {
10861108
return &fakeSubResourceClient{client: c, subResource: subResource}
10871109
}
10881110

1089-
func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor metav1.Object) error {
1111+
func (c *fakeClient) deleteObjectLocked(gvr schema.GroupVersionResource, accessor metav1.Object) error {
10901112
old, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName())
10911113
if err == nil {
10921114
oldAccessor, err := meta.Accessor(old)
@@ -1181,7 +1203,7 @@ func (sw *fakeSubResourceClient) Update(ctx context.Context, obj client.Object,
11811203

11821204
switch sw.subResource {
11831205
case subResourceScale:
1184-
if err := sw.client.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
1206+
if err := sw.client.Get(ctx, client.ObjectKeyFromObject(obj), obj.DeepCopyObject().(client.Object)); err != nil {
11851207
return err
11861208
}
11871209
if updateOptions.SubResourceBody == nil {

0 commit comments

Comments
 (0)