Repository: vmware/govmomi
Pull request #3578 (2024-10-06 22:47:18)

vcsim: add *Context param to UpdateObject interface method
This fixes a race recently introduced in #3574 , where PropertyFilter's `sync` field needs locking.
Since merging some PR tests have failed, such as [PR 3579]:
(https://github.com/vmware/govmomi/actions/runs/11214574334/job/31169931284?pr=3579)
```
WARNING: DATA RACE
Read at 0x00c0007f03e0 by goroutine 244:
  github.com/vmware/govmomi/simulator.(*PropertyFilter).update()
      /home/runner/work/govmomi/govmomi/simulator/property_filter.go:77 +0x1353
  github.com/vmware/govmomi/simulator.(*PropertyCollector).WaitForUpdatesEx()
      /home/runner/work/govmomi/govmomi/simulator/property_collector.go:953 +0x134e
...
Previous write at 0x00c0007f03e0 by goroutine 231:
  github.com/vmware/govmomi/simulator.(*PropertyFilter).UpdateObject()
      /home/runner/work/govmomi/govmomi/simulator/property_filter.go:47 +0x190
  github.com/vmware/govmomi/simulator.(*Registry).Update.func1()
      /home/runner/work/govmomi/govmomi/simulator/registry.go:324 +0x7e
```
Also fixes an existing race when destroying PropertyCollector and its PropertyFilters, seen locally while working on this fix:
```
WARNING: DATA RACE
Read at 0x00c000c28c30 by goroutine 6769:
  github.com/vmware/govmomi/simulator.(*PropertyCollector).apply()
      /Users/dougm/go/src/github.com/vmware/govmomi/simulator/property_collector.go:784 +0x5a
  github.com/vmware/govmomi/simulator.(*PropertyCollector).WaitForUpdatesEx.func1()
      /Users/dougm/go/src/github.com/vmware/govmomi/simulator/property_collector.go:898 +0x58
...
Previous write at 0x00c000c28c30 by goroutine 7095:
  github.com/vmware/govmomi/simulator.RemoveReference()
      /Users/dougm/go/src/github.com/vmware/govmomi/simulator/registry.go:476 +0x333
  github.com/vmware/govmomi/simulator.(*PropertyFilter).DestroyPropertyFilter()
      /Users/dougm/go/src/github.com/vmware/govmomi/simulator/property_filter.go:60 +0xa7
  github.com/vmware/govmomi/simulator.(*PropertyCollector).DestroyPropertyCollector()
      /Users/dougm/go/src/github.com/vmware/govmomi/simulator/property_collector.go:601 +0x1c8
```



Patch:
diff --git a/simulator/container_virtual_machine.go b/simulator/container_virtual_machine.go
index a44ad8b9..5a0b7753 100644
--- a/simulator/container_virtual_machine.go
+++ b/simulator/container_virtual_machine.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2023-2023 VMware, Inc. All Rights Reserved.
+Copyright (c) 2023-2024 VMware, Inc. All Rights Reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -193,7 +193,7 @@ func (svm *simVM) start(ctx *Context) error {
 		if err != nil {
 			log.Printf("%s %s: %s", svm.vm.Name, "start", err)
 		} else {
-			ctx.Map.Update(svm.vm, toolsRunning)
+			ctx.Update(svm.vm, toolsRunning)
 		}
 
 		return err
@@ -287,7 +287,7 @@ func (svm *simVM) start(ctx *Context) error {
 		return err
 	}
 
-	ctx.Map.Update(svm.vm, toolsRunning)
+	ctx.Update(svm.vm, toolsRunning)
 
 	svm.vm.logPrintf("%s: %s", args, svm.c.id)
 
@@ -345,7 +345,7 @@ func (svm *simVM) stop(ctx *Context) error {
 		return err
 	}
 
-	ctx.Map.Update(svm.vm, toolsNotRunning)
+	ctx.Update(svm.vm, toolsNotRunning)
 
 	return nil
 }
@@ -363,7 +363,7 @@ func (svm *simVM) pause(ctx *Context) error {
 		return err
 	}
 
-	ctx.Map.Update(svm.vm, toolsNotRunning)
+	ctx.Update(svm.vm, toolsNotRunning)
 
 	return nil
 }
@@ -381,7 +381,7 @@ func (svm *simVM) restart(ctx *Context) error {
 		return err
 	}
 
-	ctx.Map.Update(svm.vm, toolsRunning)
+	ctx.Update(svm.vm, toolsRunning)
 
 	return nil
 }
diff --git a/simulator/dvs.go b/simulator/dvs.go
index 9accd85b..45de0c38 100644
--- a/simulator/dvs.go
+++ b/simulator/dvs.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2017 VMware, Inc. All Rights Reserved.
+Copyright (c) 2017-2024 VMware, Inc. All Rights Reserved.
 
 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
+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,
@@ -166,7 +166,7 @@ func (s *DistributedVirtualSwitch) AddDVPortgroupTask(ctx *Context, c *types.Add
 
 				parent := ctx.Map.Get(*host.HostSystem.Parent)
 				computeNetworks := append(hostParent(&host.HostSystem).Network, pg.Reference())
-				ctx.Map.Update(parent, []types.PropertyChange{
+				ctx.Update(parent, []types.PropertyChange{
 					{Name: "network", Val: computeNetworks},
 				})
 			}
@@ -176,7 +176,7 @@ func (s *DistributedVirtualSwitch) AddDVPortgroupTask(ctx *Context, c *types.Add
 			})
 		}
 
-		ctx.Map.Update(s, []types.PropertyChange{
+		ctx.Update(s, []types.PropertyChange{
 			{Name: "portgroup", Val: portgroups},
 			{Name: "summary.portgroupName", Val: portgroupNames},
 		})
@@ -212,7 +212,7 @@ func (s *DistributedVirtualSwitch) ReconfigureDvsTask(ctx *Context, req *types.R
 				}
 
 				hostNetworks := append(host.Network, s.Portgroup...)
-				ctx.Map.Update(host, []types.PropertyChange{
+				ctx.Update(host, []types.PropertyChange{
 					{Name: "network", Val: hostNetworks},
 				})
 				members = append(members, member.Host)
@@ -224,14 +224,14 @@ func (s *DistributedVirtualSwitch) ReconfigureDvsTask(ctx *Context, req *types.R
 					pgs = append(pgs, ref)
 
 					pgHosts := append(pg.Host, member.Host)
-					ctx.Map.Update(pg, []types.PropertyChange{
+					ctx.Update(pg, []types.PropertyChange{
 						{Name: "host", Val: pgHosts},
 					})
 
 					cr := hostParent(&host.HostSystem)
 					if FindReference(cr.Network, ref) == nil {
 						computeNetworks := append(cr.Network, ref)
-						ctx.Map.Update(parent, []types.PropertyChange{
+						ctx.Update(parent, []types.PropertyChange{
 							{Name: "network", Val: computeNetworks},
 						})
 					}
@@ -263,7 +263,7 @@ func (s *DistributedVirtualSwitch) ReconfigureDvsTask(ctx *Context, req *types.R
 			}
 		}
 
-		ctx.Map.Update(s, []types.PropertyChange{
+		ctx.Update(s, []types.PropertyChange{
 			{Name: "summary.hostMember", Val: members},
 		})
 
diff --git a/simulator/entity.go b/simulator/entity.go
index 183e954f..403bbb47 100644
--- a/simulator/entity.go
+++ b/simulator/entity.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2017 VMware, Inc. All Rights Reserved.
+Copyright (c) 2017-2024 VMware, Inc. All Rights Reserved.
 
 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
+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,
@@ -34,7 +34,7 @@ func RenameTask(ctx *Context, e mo.Entity, r *types.Rename_Task, dup ...bool) so
 			}
 		}
 
-		ctx.Map.Update(e, []types.PropertyChange{{Name: "name", Val: r.NewName}})
+		ctx.Update(e, []types.PropertyChange{{Name: "name", Val: r.NewName}})
 
 		return nil, nil
 	})
diff --git a/simulator/event_manager.go b/simulator/event_manager.go
index 94edea51..4195af72 100644
--- a/simulator/event_manager.go
+++ b/simulator/event_manager.go
@@ -161,7 +161,7 @@ func (m *EventManager) PostEvent(ctx *Context, req *types.PostEvent) soap.HasFau
 		ctx.WithLock(c, func() {
 			if c.eventMatches(ctx, req.EventToPost) {
 				pushHistory(c.page, req.EventToPost)
-				ctx.Map.Update(c, []types.PropertyChange{{Name: "latestPage", Val: c.GetLatestPage()}})
+				ctx.Update(c, []types.PropertyChange{{Name: "latestPage", Val: c.GetLatestPage()}})
 			}
 		})
 	}
diff --git a/simulator/extension_manager.go b/simulator/extension_manager.go
index 84b7c6b0..b8d45461 100644
--- a/simulator/extension_manager.go
+++ b/simulator/extension_manager.go
@@ -98,7 +98,7 @@ func (m *ExtensionManager) RegisterExtension(ctx *Context, req *types.RegisterEx
 	m.ExtensionList = append(m.ExtensionList, req.Extension)
 
 	f := mo.Field{Path: "extensionList", Key: req.Extension.Key}
-	ctx.Map.Update(m, []types.PropertyChange{
+	ctx.Update(m, []types.PropertyChange{
 		{Name: f.Path, Val: m.ExtensionList},
 		{Name: f.String(), Val: req.Extension, Op: types.PropertyChangeOpAdd},
 	})
@@ -114,7 +114,7 @@ func (m *ExtensionManager) UnregisterExtension(ctx *Context, req *types.Unregist
 			m.ExtensionList = append(m.ExtensionList[:i], m.ExtensionList[i+1:]...)
 
 			f := mo.Field{Path: "extensionList", Key: req.ExtensionKey}
-			ctx.Map.Update(m, []types.PropertyChange{
+			ctx.Update(m, []types.PropertyChange{
 				{Name: f.Path, Val: m.ExtensionList},
 				{Name: f.String(), Op: types.PropertyChangeOpRemove},
 			})
@@ -137,7 +137,7 @@ func (m *ExtensionManager) UpdateExtension(ctx *Context, req *types.UpdateExtens
 			m.ExtensionList[i] = req.Extension
 
 			f := mo.Field{Path: "extensionList", Key: req.Extension.Key}
-			ctx.Map.Update(m, []types.PropertyChange{
+			ctx.Update(m, []types.PropertyChange{
 				{Name: f.Path, Val: m.ExtensionList},
 				{Name: f.String(), Val: req.Extension},
 			})
diff --git a/simulator/folder.go b/simulator/folder.go
index dcf2dfa7..d4e2c281 100644
--- a/simulator/folder.go
+++ b/simulator/folder.go
@@ -301,7 +301,7 @@ func (f *Folder) CreateDatacenter(ctx *Context, c *types.CreateDatacenter) soap.
 	if folderHasChildType(&f.Folder, "Datacenter") && folderHasChildType(&f.Folder, "Folder") {
 		dc := NewDatacenter(ctx, &f.Folder)
 
-		ctx.Map.Update(dc, []types.PropertyChange{
+		ctx.Update(dc, []types.PropertyChange{
 			{Name: "name", Val: c.Name},
 		})
 
@@ -490,7 +490,7 @@ func (c *createVM) Run(task *Task) (types.AnyType, types.BaseMethodFault) {
 
 	vm.RefreshStorageInfo(c.ctx, nil)
 
-	c.ctx.Map.Update(vm, []types.PropertyChange{
+	c.ctx.Update(vm, []types.PropertyChange{
 		{Name: "name", Val: c.req.Config.Name},
 	})
 
diff --git a/simulator/http_nfc_lease.go b/simulator/http_nfc_lease.go
index 54d312ca..3b67a498 100644
--- a/simulator/http_nfc_lease.go
+++ b/simulator/http_nfc_lease.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2019-2023 VMware, Inc. All Rights Reserved.
+Copyright (c) 2019-2024 VMware, Inc. All Rights Reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -108,7 +108,7 @@ func ServeNFC(w http.ResponseWriter, r *http.Request) {
 
 func (l *HttpNfcLease) error(ctx *Context, err *types.LocalizedMethodFault) {
 	ctx.WithLock(l, func() {
-		ctx.Map.Update(l, []types.PropertyChange{
+		ctx.Update(l, []types.PropertyChange{
 			{Name: "state", Val: types.HttpNfcLeaseStateError},
 			{Name: "error", Val: err},
 		})
@@ -124,7 +124,7 @@ func (l *HttpNfcLease) ready(ctx *Context, entity types.ManagedObjectReference,
 	}
 
 	ctx.WithLock(l, func() {
-		ctx.Map.Update(l, []types.PropertyChange{
+		ctx.Update(l, []types.PropertyChange{
 			{Name: "state", Val: types.HttpNfcLeaseStateReady},
 			{Name: "info", Val: info},
 		})
diff --git a/simulator/property_collector.go b/simulator/property_collector.go
index 4fee683e..ed15dbfe 100644
--- a/simulator/property_collector.go
+++ b/simulator/property_collector.go
@@ -597,8 +597,7 @@ func (pc *PropertyCollector) DestroyPropertyCollector(ctx *Context, c *types.Des
 	body := &methods.DestroyPropertyCollectorBody{}
 
 	for _, ref := range pc.Filter {
-		filter := ctx.Session.Get(ref).(*PropertyFilter)
-		filter.DestroyPropertyFilter(ctx, &types.DestroyPropertyFilter{This: ref})
+		ctx.Session.Remove(ctx, ref)
 	}
 
 	ctx.Session.Remove(ctx, c.This)
@@ -764,7 +763,7 @@ func (pc *PropertyCollector) PutObject(o mo.Reference) {
 	})
 }
 
-func (pc *PropertyCollector) UpdateObject(o mo.Reference, changes []types.PropertyChange) {
+func (pc *PropertyCollector) UpdateObject(_ *Context, o mo.Reference, changes []types.PropertyChange) {
 	pc.update(types.ObjectUpdate{
 		Obj:       o.Reference(),
 		Kind:      types.ObjectUpdateKindModify,
@@ -782,7 +781,10 @@ func (pc *PropertyCollector) RemoveObject(_ *Context, ref types.ManagedObjectRef
 
 func (pc *PropertyCollector) apply(ctx *Context, update *types.UpdateSet) types.BaseMethodFault {
 	for _, ref := range pc.Filter {
-		filter := ctx.Session.Get(ref).(*PropertyFilter)
+		filter, ok := ctx.Session.Get(ref).(*PropertyFilter)
+		if !ok {
+			continue
+		}
 
 		res, fault := filter.collect(ctx)
 		if fault != nil {
diff --git a/simulator/property_filter.go b/simulator/property_filter.go
index 98100608..e69446ac 100644
--- a/simulator/property_filter.go
+++ b/simulator/property_filter.go
@@ -34,7 +34,7 @@ type PropertyFilter struct {
 	sync bool
 }
 
-func (f *PropertyFilter) UpdateObject(o mo.Reference, changes []types.PropertyChange) {
+func (f *PropertyFilter) UpdateObject(ctx *Context, o mo.Reference, changes []types.PropertyChange) {
 	// A PropertyFilter's traversal spec is "applied" on the initial call to WaitForUpdates,
 	// with matching objects tracked in the `refs` field.
 	// New and deleted objects matching the filter are accounted for within PropertyCollector.
@@ -44,7 +44,7 @@ func (f *PropertyFilter) UpdateObject(o mo.Reference, changes []types.PropertyCh
 
 	for _, set := range f.Spec.ObjectSet {
 		if set.Obj == ref && len(set.SelectSet) != 0 {
-			f.sync = true
+			ctx.WithLock(f, func() { f.sync = true })
 			break
 		}
 	}
@@ -74,11 +74,13 @@ func (f *PropertyFilter) collect(ctx *Context) (*types.RetrieveResult, types.Bas
 }
 
 func (f *PropertyFilter) update(ctx *Context) {
-	if f.sync {
-		f.sync = false
-		clear(f.refs)
-		_, _ = f.collect(ctx)
-	}
+	ctx.WithLock(f, func() {
+		if f.sync {
+			f.sync = false
+			clear(f.refs)
+			_, _ = f.collect(ctx)
+		}
+	})
 }
 
 // matches returns true if the change matches one of the filter Spec.PropSet
diff --git a/simulator/registry.go b/simulator/registry.go
index 70146e72..8fbfea18 100644
--- a/simulator/registry.go
+++ b/simulator/registry.go
@@ -61,7 +61,7 @@ var Map = NewRegistry()
 type RegisterObject interface {
 	mo.Reference
 	PutObject(mo.Reference)
-	UpdateObject(mo.Reference, []types.PropertyChange)
+	UpdateObject(*Context, mo.Reference, []types.PropertyChange)
 	RemoveObject(*Context, types.ManagedObjectReference)
 }
 
@@ -305,7 +305,7 @@ func (r *Registry) Remove(ctx *Context, item types.ManagedObjectReference) {
 // such as any PropertyCollector instances with in-progress WaitForUpdates calls.
 // The changes are also applied to the given object via mo.ApplyPropertyChange,
 // so there is no need to set object fields directly.
-func (r *Registry) Update(obj mo.Reference, changes []types.PropertyChange) {
+func (r *Registry) Update(ctx *Context, obj mo.Reference, changes []types.PropertyChange) {
 	for i := range changes {
 		if changes[i].Op == "" {
 			changes[i].Op = types.PropertyChangeOpAssign
@@ -321,13 +321,13 @@ func (r *Registry) Update(obj mo.Reference, changes []types.PropertyChange) {
 	mo.ApplyPropertyChange(val, changes)
 
 	r.applyHandlers(func(o RegisterObject) {
-		o.UpdateObject(val, changes)
+		o.UpdateObject(ctx, val, changes)
 	})
 }
 
 func (r *Registry) AtomicUpdate(ctx *Context, obj mo.Reference, changes []types.PropertyChange) {
 	r.WithLock(ctx, obj, func() {
-		r.Update(obj, changes)
+		ctx.Update(obj, changes)
 	})
 }
 
diff --git a/simulator/session_manager.go b/simulator/session_manager.go
index 02e2212c..85ed8153 100644
--- a/simulator/session_manager.go
+++ b/simulator/session_manager.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2017-2023 VMware, Inc. All Rights Reserved.
+Copyright (c) 2017-2024 VMware, Inc. All Rights Reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -415,6 +415,10 @@ func (c *Context) WithLock(obj mo.Reference, f func()) {
 	c.Map.WithLock(c, obj, f)
 }
 
+func (c *Context) Update(obj mo.Reference, changes []types.PropertyChange) {
+	c.Map.Update(c, obj, changes)
+}
+
 // postEvent wraps EventManager.PostEvent for internal use, with a lock on the EventManager.
 func (c *Context) postEvent(events ...types.BaseEvent) {
 	m := c.Map.EventManager()
diff --git a/simulator/snapshot.go b/simulator/snapshot.go
index 84e40427..4abfbfcd 100644
--- a/simulator/snapshot.go
+++ b/simulator/snapshot.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2017-2023 VMware, Inc. All Rights Reserved.
+Copyright (c) 2017-2024 VMware, Inc. All Rights Reserved.
 
 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
+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,
@@ -139,7 +139,7 @@ func (v *VirtualMachineSnapshot) RemoveSnapshotTask(ctx *Context, req *types.Rem
 
 			ctx.Map.Get(req.This).(*VirtualMachineSnapshot).removeSnapshotFiles(ctx)
 
-			ctx.Map.Update(vm, changes)
+			ctx.Update(vm, changes)
 		})
 
 		ctx.Map.Remove(ctx, req.This)
@@ -160,7 +160,7 @@ func (v *VirtualMachineSnapshot) RevertToSnapshotTask(ctx *Context, req *types.R
 
 		ctx.WithLock(vm, func() {
 			vm.DataSets = copyDataSetsForVmClone(v.DataSets)
-			ctx.Map.Update(vm, []types.PropertyChange{
+			ctx.Update(vm, []types.PropertyChange{
 				{Name: "snapshot.currentSnapshot", Val: v.Self},
 			})
 		})
diff --git a/simulator/task.go b/simulator/task.go
index a08b0900..d41274e6 100644
--- a/simulator/task.go
+++ b/simulator/task.go
@@ -214,7 +214,7 @@ func (t *Task) SetTaskState(ctx *Context, req *types.SetTaskState) soap.HasFault
 		}
 	}
 
-	ctx.Map.Update(t, changes)
+	ctx.Update(t, changes)
 
 	body.Res = new(types.SetTaskStateResponse)
 	return body
@@ -228,7 +228,7 @@ func (t *Task) SetTaskDescription(ctx *Context, req *types.SetTaskDescription) s
 		return body
 	}
 
-	ctx.Map.Update(t, []types.PropertyChange{{Name: "info.description", Val: req.Description}})
+	ctx.Update(t, []types.PropertyChange{{Name: "info.description", Val: req.Description}})
 
 	body.Res = new(types.SetTaskDescriptionResponse)
 	return body
@@ -242,7 +242,7 @@ func (t *Task) UpdateProgress(ctx *Context, req *types.UpdateProgress) soap.HasF
 		return body
 	}
 
-	ctx.Map.Update(t, []types.PropertyChange{{Name: "info.progress", Val: req.PercentDone}})
+	ctx.Update(t, []types.PropertyChange{{Name: "info.progress", Val: req.PercentDone}})
 
 	body.Res = new(types.UpdateProgressResponse)
 	return body
@@ -266,7 +266,7 @@ func (t *Task) CancelTask(ctx *Context, req *types.CancelTask) soap.HasFault {
 		}},
 	}
 
-	ctx.Map.Update(t, changes)
+	ctx.Update(t, changes)
 
 	body.Res = new(types.CancelTaskResponse)
 	return body
diff --git a/simulator/task_manager.go b/simulator/task_manager.go
index a81d1e0d..f04787bc 100644
--- a/simulator/task_manager.go
+++ b/simulator/task_manager.go
@@ -79,7 +79,7 @@ func (m *TaskManager) PutObject(obj mo.Reference) {
 	// - TaskHistoryCollector instances, if Filter matches
 	// - $MO.RecentTask
 	m.Lock()
-	ctx.Map.Update(m, recentTask(m.RecentTask, task.Self))
+	ctx.Update(m, recentTask(m.RecentTask, task.Self))
 
 	pushHistory(m.history.page, task)
 
@@ -88,7 +88,7 @@ func (m *TaskManager) PutObject(obj mo.Reference) {
 		ctx.WithLock(c, func() {
 			if c.taskMatches(ctx, &task.Info) {
 				pushHistory(c.page, task)
-				ctx.Map.Update(c, []types.PropertyChange{{Name: "latestPage", Val: c.GetLatestPage()}})
+				ctx.Update(c, []types.PropertyChange{{Name: "latestPage", Val: c.GetLatestPage()}})
 			}
 		})
 	}
@@ -96,7 +96,7 @@ func (m *TaskManager) PutObject(obj mo.Reference) {
 
 	entity := ctx.Map.Get(*task.Info.Entity)
 	if e, ok := entity.(mo.Entity); ok {
-		ctx.Map.Update(entity, recentTask(e.Entity().RecentTask, task.Self))
+		ctx.Update(entity, recentTask(e.Entity().RecentTask, task.Self))
 	}
 }
 
@@ -109,7 +109,7 @@ func taskStateChanged(pc []types.PropertyChange) bool {
 	return false
 }
 
-func (m *TaskManager) UpdateObject(obj mo.Reference, pc []types.PropertyChange) {
+func (m *TaskManager) UpdateObject(ctx *Context, obj mo.Reference, pc []types.PropertyChange) {
 	task, ok := obj.(*mo.Task)
 	if !ok {
 		return
@@ -125,10 +125,9 @@ func (m *TaskManager) UpdateObject(obj mo.Reference, pc []types.PropertyChange)
 	m.Lock()
 	for _, hc := range m.history.collectors {
 		c := hc.(*TaskHistoryCollector)
-		ctx := SpoofContext()
 		ctx.WithLock(c, func() {
 			if c.hasTask(ctx, &task.Info) {
-				ctx.Map.Update(c, []types.PropertyChange{{Name: "latestPage", Val: c.GetLatestPage()}})
+				ctx.Update(c, []types.PropertyChange{{Name: "latestPage", Val: c.GetLatestPage()}})
 			}
 		})
 	}
diff --git a/simulator/view_manager.go b/simulator/view_manager.go
index 0ed58926..969ca1d1 100644
--- a/simulator/view_manager.go
+++ b/simulator/view_manager.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2017-2023 VMware, Inc. All Rights Reserved.
+Copyright (c) 2017-2024 VMware, Inc. All Rights Reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -213,7 +213,7 @@ func (v *ContainerView) PutObject(obj mo.Reference) {
 	ref := obj.Reference()
 
 	if v.include(ref) && v.find(v.root, ref, types.NewBool(false)) {
-		Map.Update(v, []types.PropertyChange{{Name: "view", Val: append(v.View, ref)}})
+		SpoofContext().Update(v, []types.PropertyChange{{Name: "view", Val: append(v.View, ref)}})
 	}
 }
 
@@ -221,7 +221,7 @@ func (v *ContainerView) RemoveObject(ctx *Context, obj types.ManagedObjectRefere
 	ctx.Map.RemoveReference(ctx, v, &v.View, obj)
 }
 
-func (*ContainerView) UpdateObject(mo.Reference, []types.PropertyChange) {}
+func (*ContainerView) UpdateObject(*Context, mo.Reference, []types.PropertyChange) {}
 
 func (m *ViewManager) CreateListView(ctx *Context, req *types.CreateListView) soap.HasFault {
 	body := new(methods.CreateListViewBody)
@@ -246,7 +246,7 @@ type ListView struct {
 }
 
 func (v *ListView) update(ctx *Context) {
-	ctx.Map.Update(v, []types.PropertyChange{{Name: "view", Val: v.View}})
+	ctx.Update(v, []types.PropertyChange{{Name: "view", Val: v.View}})
 }
 
 func (v *ListView) add(ctx *Context, refs []types.ManagedObjectReference) []types.ManagedObjectReference {
diff --git a/simulator/virtual_machine.go b/simulator/virtual_machine.go
index a58354cb..a596798d 100644
--- a/simulator/virtual_machine.go
+++ b/simulator/virtual_machine.go
@@ -544,7 +544,7 @@ func (vm *VirtualMachine) applyExtraConfig(ctx *Context, spec *types.VirtualMach
 	}
 
 	change := types.PropertyChange{Name: field.Path, Val: vm.Config.ExtraConfig}
-	ctx.Map.Update(vm, append(changes, change))
+	ctx.Update(vm, append(changes, change))
 
 	return fault
 }
@@ -1318,7 +1318,7 @@ func (vm *VirtualMachine) configureDevice(
 			}
 		}
 
-		ctx.Map.Update(vm, []types.PropertyChange{
+		ctx.Update(vm, []types.PropertyChange{
 			{Name: "summary.config.numEthernetCards", Val: vm.Summary.Config.NumEthernetCards + 1},
 			{Name: "network", Val: append(vm.Network, net)},
 		})
@@ -1398,7 +1398,7 @@ func (vm *VirtualMachine) configureDevice(
 				return err
 			}
 
-			ctx.Map.Update(vm, []types.PropertyChange{
+			ctx.Update(vm, []types.PropertyChange{
 				{Name: "summary.config.numVirtualDisks", Val: vm.Summary.Config.NumVirtualDisks + 1},
 			})
 
@@ -1537,7 +1537,7 @@ func (vm *VirtualMachine) removeDevice(ctx *Context, devices object.VirtualDevic
 					ctask.Wait()
 				}
 			}
-			ctx.Map.Update(vm, []types.PropertyChange{
+			ctx.Update(vm, []types.PropertyChange{
 				{Name: "summary.config.numVirtualDisks", Val: vm.Summary.Config.NumVirtualDisks - 1},
 			})
 
@@ -1562,7 +1562,7 @@ func (vm *VirtualMachine) removeDevice(ctx *Context, devices object.VirtualDevic
 
 			networks := vm.Network
 			RemoveReference(&networks, net)
-			ctx.Map.Update(vm, []types.PropertyChange{
+			ctx.Update(vm, []types.PropertyChange{
 				{Name: "summary.config.numEthernetCards", Val: vm.Summary.Config.NumEthernetCards - 1},
 				{Name: "network", Val: networks},
 			})
@@ -1686,7 +1686,7 @@ func (vm *VirtualMachine) updateCrypto(
 			keyID = generateKeyForProvider(ctx, providerID.Id)
 		}
 
-		ctx.Map.Update(vm, []types.PropertyChange{
+		ctx.Update(vm, []types.PropertyChange{
 			{
 				Name: configKeyId,
 				Op:   types.PropertyChangeOpAssign,
@@ -1710,7 +1710,7 @@ func (vm *VirtualMachine) updateCrypto(
 		if err := assertEncrypted(); err != nil {
 			return err
 		}
-		ctx.Map.Update(vm, []types.PropertyChange{
+		ctx.Update(vm, []types.PropertyChange{
 			{
 				Name: configKeyId,
 				Op:   types.PropertyChangeOpRemove,
@@ -1763,7 +1763,7 @@ func (vm *VirtualMachine) updateCrypto(
 			keyID = generateKeyForProvider(ctx, providerID.Id)
 		}
 
-		ctx.Map.Update(vm, []types.PropertyChange{
+		ctx.Update(vm, []types.PropertyChange{
 			{
 				Name: configKeyId,
 				Op:   types.PropertyChangeOpAssign,
@@ -1876,7 +1876,7 @@ func (vm *VirtualMachine) configureDevices(ctx *Context, spec *types.VirtualMach
 
 	if len(changes) != 0 {
 		change := types.PropertyChange{Name: field.Path, Val: []types.BaseVirtualDevice(devices)}
-		ctx.Map.Update(vm, append(changes, change))
+		ctx.Update(vm, append(changes, change))
 	}
 
 	err = vm.updateDiskLayouts()
@@ -1975,7 +1975,7 @@ func (c *powerVMTask) Run(task *Task) (types.AnyType, types.BaseMethodFault) {
 		}
 	}
 
-	c.ctx.Map.Update(c.VirtualMachine, []types.PropertyChange{
+	c.ctx.Update(c.VirtualMachine, []types.PropertyChange{
 		{Name: "runtime.powerState", Val: c.state},
 		{Name: "summary.runtime.powerState", Val: c.state},
 		{Name: "summary.runtime.bootTime", Val: boot},
@@ -2209,7 +2209,7 @@ func (vm *VirtualMachine) UpgradeVMTask(ctx *Context, req *types.UpgradeVM_Task)
 			return nil, &types.InvalidArgument{}
 		}
 
-		ctx.Map.Update(vm, []types.PropertyChange{
+		ctx.Update(vm, []types.PropertyChange{
 			{
 				Name: "config.version", Val: targetVersion.String(),
 			},
@@ -2534,7 +2534,7 @@ func (vm *VirtualMachine) RelocateVMTask(ctx *Context, req *types.RelocateVM_Tas
 			SourceDatastore:  ctx.Map.Get(vm.Datastore[0]).(*Datastore).eventArgument(),
 		})
 
-		ctx.Map.Update(vm, changes)
+		ctx.Update(vm, changes)
 
 		return nil, nil
 	})
@@ -2625,7 +2625,7 @@ func (vm *VirtualMachine) customize(ctx *Context) {
 	}
 
 	vm.imc = nil
-	ctx.Map.Update(vm, changes)
+	ctx.Update(vm, changes)
 	ctx.postEvent(&types.CustomizationSucceeded{CustomizationEvent: event})
 }
 
@@ -2713,7 +2713,7 @@ func (vm *VirtualMachine) CreateSnapshotTask(ctx *Context, req *types.CreateSnap
 		snapshot.createSnapshotFiles()
 
 		changes = append(changes, types.PropertyChange{Name: "snapshot.currentSnapshot", Val: snapshot.Self})
-		ctx.Map.Update(vm, changes)
+		ctx.Update(vm, changes)
 
 		return snapshot.Self, nil
 	})
@@ -2755,7 +2755,7 @@ func (vm *VirtualMachine) RemoveAllSnapshotsTask(ctx *Context, req *types.Remove
 
 		refs := allSnapshotsInTree(vm.Snapshot.RootSnapshotList)
 
-		ctx.Map.Update(vm, []types.PropertyChange{
+		ctx.Update(vm, []types.PropertyChange{
 			{Name: "snapshot", Val: nil},
 			{Name: "rootSnapshot", Val: nil},
 		})
@@ -2850,7 +2850,7 @@ func (vm *VirtualMachine) ShutdownGuest(ctx *Context, c *types.ShutdownGuest) so
 	_ = CreateTask(vm, "shutdownGuest", func(*Task) (types.AnyType, types.BaseMethodFault) {
 		vm.svm.stop(ctx)
 
-		ctx.Map.Update(vm, []types.PropertyChange{
+		ctx.Update(vm, []types.PropertyChange{
 			{Name: "runtime.powerState", Val: types.VirtualMachinePowerStatePoweredOff},
 			{Name: "summary.runtime.powerState", Val: types.VirtualMachinePowerStatePoweredOff},
 		})
@@ -2883,7 +2883,7 @@ func (vm *VirtualMachine) StandbyGuest(ctx *Context, c *types.StandbyGuest) soap
 	_ = CreateTask(vm, "standbyGuest", func(*Task) (types.AnyType, types.BaseMethodFault) {
 		vm.svm.pause(ctx)
 
-		ctx.Map.Update(vm, []types.PropertyChange{
+		ctx.Update(vm, []types.PropertyChange{
 			{Name: "runtime.powerState", Val: types.VirtualMachinePowerStateSuspended},
 			{Name: "summary.runtime.powerState", Val: types.VirtualMachinePowerStateSuspended},
 		})
@@ -3058,7 +3058,7 @@ func changeTrackingSupported(spec *types.VirtualMachineConfigSpec) bool {
 
 func (vm *VirtualMachine) updateLastModifiedAndChangeVersion(ctx *Context) {
 	modified := time.Now()
-	ctx.Map.Update(vm, []types.PropertyChange{
+	ctx.Update(vm, []types.PropertyChange{
 		{
 			Name: "config.changeVersion",
 			Val:  fmt.Sprintf("%d", modified.UnixNano()),


Test patch:
diff --git a/object/vm_guest_test.go b/object/vm_guest_test.go
index eebef805..c525334b 100644
--- a/object/vm_guest_test.go
+++ b/object/vm_guest_test.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2019 VMware, Inc. All Rights Reserved.
+Copyright (c) 2019-2024 VMware, Inc. All Rights Reserved.
 
 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
+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,
@@ -37,8 +37,21 @@ func TestVirtualMachineWaitForIP(t *testing.T) {
 			return err
 		}
 
-		obj := simulator.Map.Get(vm.Reference()).(*simulator.VirtualMachine)
-		obj.Guest.IpAddress = "fe80::250:56ff:fe97:2458"
+		reconfig := func(ip string) error {
+			task, err := vm.Reconfigure(ctx, types.VirtualMachineConfigSpec{
+				ExtraConfig: []types.BaseOptionValue{
+					&types.OptionValue{Key: "SET.guest.ipAddress", Value: ip},
+				},
+			})
+			if err != nil {
+				return err
+			}
+			return task.Wait(ctx)
+		}
+
+		if err := reconfig("fe80::250:56ff:fe97:2458"); err != nil {
+			return err
+		}
 
 		ip, err := vm.WaitForIP(ctx)
 		if err != nil {
@@ -49,7 +62,7 @@ func TestVirtualMachineWaitForIP(t *testing.T) {
 			t.Errorf("expected v6 ip, but %q is v4", ip)
 		}
 
-		delay := time.Second
+		delay := time.Second / 2
 		var wg sync.WaitGroup
 
 		wg.Add(1)
@@ -57,12 +70,9 @@ func TestVirtualMachineWaitForIP(t *testing.T) {
 			defer wg.Done()
 			t.Logf("delaying map update for %v", delay)
 			time.Sleep(delay)
-
-			simulator.Map.WithLock(simulator.SpoofContext(), obj.Reference(), func() {
-				simulator.Map.Update(obj, []types.PropertyChange{
-					{Name: "guest.ipAddress", Val: "10.0.0.1"},
-				})
-			})
+			if err := reconfig("10.0.0.1"); err != nil {
+				t.Logf("reconfig error: %s", err)
+			}
 		}()
 
 		ip, err = vm.WaitForIP(ctx, true)