Skip to content

Commit 013f46f

Browse files
authored
Merge pull request #2992 from k8s-infra-cherrypick-robot/cherry-pick-2980-to-release-0.19
[release-0.19] 🐛 Fakeclient: Fix TOCTOU races
2 parents aa14005 + 4421425 commit 013f46f

File tree

2 files changed

+309
-11
lines changed

2 files changed

+309
-11
lines changed

pkg/client/fake/client.go

+30-8
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,21 @@ type versionedTracker struct {
6868
}
6969

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

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

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

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

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

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

778-
return c.deleteObject(gvr, accessor)
792+
return c.deleteObjectLocked(gvr, accessor)
779793
}
780794

781795
func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
@@ -793,6 +807,9 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ..
793807
}
794808
}
795809

810+
c.trackerWriteLock.Lock()
811+
defer c.trackerWriteLock.Unlock()
812+
796813
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
797814
o, err := c.tracker.List(gvr, gvk, dcOptions.Namespace)
798815
if err != nil {
@@ -812,7 +829,7 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ..
812829
if err != nil {
813830
return err
814831
}
815-
err = c.deleteObject(gvr, accessor)
832+
err = c.deleteObjectLocked(gvr, accessor)
816833
if err != nil {
817834
return err
818835
}
@@ -842,6 +859,9 @@ func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.Upd
842859
if err != nil {
843860
return err
844861
}
862+
863+
c.trackerWriteLock.Lock()
864+
defer c.trackerWriteLock.Unlock()
845865
return c.tracker.update(gvr, obj, accessor.GetNamespace(), isStatus, false, *updateOptions.AsUpdateOptions())
846866
}
847867

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

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

1088-
func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor metav1.Object) error {
1110+
func (c *fakeClient) deleteObjectLocked(gvr schema.GroupVersionResource, accessor metav1.Object) error {
10891111
old, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName())
10901112
if err == nil {
10911113
oldAccessor, err := meta.Accessor(old)
@@ -1167,7 +1189,7 @@ func (sw *fakeSubResourceClient) Update(ctx context.Context, obj client.Object,
11671189

11681190
switch sw.subResource {
11691191
case subResourceScale:
1170-
if err := sw.client.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
1192+
if err := sw.client.Get(ctx, client.ObjectKeyFromObject(obj), obj.DeepCopyObject().(client.Object)); err != nil {
11711193
return err
11721194
}
11731195
if updateOptions.SubResourceBody == nil {

0 commit comments

Comments
 (0)