Skip to content

Commit d286c1d

Browse files
committed
add vmmem process lookup functionality + windows API abstraction
The original implementation used a brute-force approach to locate the Hyper-V memory management process. It relied on enumerating every single Process ID (PID) on the system and attempting to open each one to query its image path. This meant the system was executing expensive OS-level calls and generating ignored access-denied errors hundreds of times for unrelated processes, just to check if the name matched "vmmem". Ref- https://github.com/microsoft/hcsshim/blob/178d662f94b76b24fda758dfc41a7d9e6a2ee614/internal/uvm/stats.go#L76 The new enhancement significantly optimizes this discovery process by using windows API `CreateToolhelp32Snapshot`, which captures process names upfront without opening the processes. Now, the code iterates through the snapshot and pre-filters by the executable name. It only executes the expensive `OpenProcess` and security token checks (`LookupAccount`) if the process is already confirmed to be named `vmmem` or `vmmem.exe`. Furthermore, the updated code introduces an interface to wrap the Windows OS calls. This dependency injection allows the system calls to be easily mocked, making the function fully unit-testable without requiring a live Hyper-V virtual machine. Signed-off-by: Harsh Rawat <harshrawat@microsoft.com>
1 parent 1b6694d commit d286c1d

7 files changed

Lines changed: 758 additions & 0 deletions

File tree

internal/vm/vmutils/vmmem.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//go:build windows
2+
3+
package vmutils
4+
5+
import (
6+
"context"
7+
"errors"
8+
"iter"
9+
"strings"
10+
"unsafe"
11+
12+
"github.com/Microsoft/hcsshim/internal/log"
13+
iwin "github.com/Microsoft/hcsshim/internal/windows"
14+
15+
"github.com/Microsoft/go-winio/pkg/guid"
16+
"golang.org/x/sys/windows"
17+
)
18+
19+
const (
20+
// vmmemProcessName is the name of the Hyper-V memory management process.
21+
vmmemProcessName = "vmmem"
22+
// vmmemProcessNameExt is the name of the process with .exe extension.
23+
vmmemProcessNameExt = "vmmem.exe"
24+
// ntVirtualMachineDomain is the domain name for Hyper-V virtual machine security principals.
25+
ntVirtualMachineDomain = "NT VIRTUAL MACHINE"
26+
)
27+
28+
// allProcessEntries returns an iterator over all process entries in a Toolhelp32 snapshot.
29+
// If the snapshot cannot be created or a process entry cannot be read, the error is logged and
30+
// iteration stops.
31+
func allProcessEntries(ctx context.Context, win iwin.API) iter.Seq[*windows.ProcessEntry32] {
32+
return func(yield func(*windows.ProcessEntry32) bool) {
33+
snapshot, err := win.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
34+
if err != nil {
35+
log.G(ctx).WithError(err).Error("failed to create process snapshot")
36+
return
37+
}
38+
defer func(win iwin.API, h windows.Handle) {
39+
_ = win.CloseHandle(h)
40+
}(win, snapshot)
41+
42+
var pe32 windows.ProcessEntry32
43+
pe32.Size = uint32(unsafe.Sizeof(pe32))
44+
45+
for err = win.Process32First(snapshot, &pe32); ; err = win.Process32Next(snapshot, &pe32) {
46+
if err != nil {
47+
log.G(ctx).WithError(err).Debug("finished iterating process entries")
48+
return
49+
}
50+
if !yield(&pe32) {
51+
return
52+
}
53+
}
54+
}
55+
}
56+
57+
// LookupVMMEM locates the vmmem process for a VM given the VM ID.
58+
// It enumerates processes using Toolhelp32 to filter by name, then validates
59+
// the token using LookupAccount to match the "NT VIRTUAL MACHINE\<VM ID>" identity.
60+
func LookupVMMEM(ctx context.Context, vmID guid.GUID, win iwin.API) (windows.Handle, error) {
61+
vmIDStr := strings.ToUpper(vmID.String())
62+
log.G(ctx).WithField("vmID", vmIDStr).Debug("looking up vmmem via LookupAccount")
63+
64+
for pe32 := range allProcessEntries(ctx, win) {
65+
exeName := windows.UTF16ToString(pe32.ExeFile[:])
66+
67+
// 1. Only target processes named vmmem or vmmem.exe.
68+
if !strings.EqualFold(exeName, vmmemProcessName) && !strings.EqualFold(exeName, vmmemProcessNameExt) {
69+
continue
70+
}
71+
72+
// 2. Open the process to inspect its security token.
73+
pHandle, err := win.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pe32.ProcessID)
74+
if err != nil {
75+
continue
76+
}
77+
78+
var t windows.Token
79+
if err := win.OpenProcessToken(pHandle, windows.TOKEN_QUERY, &t); err != nil {
80+
_ = win.CloseHandle(pHandle)
81+
continue
82+
}
83+
84+
tUser, err := win.GetTokenUser(t)
85+
if err != nil {
86+
_ = win.CloseToken(t)
87+
_ = win.CloseHandle(pHandle)
88+
continue
89+
}
90+
91+
// 3. Use the OS API to resolve the SID to account and domain strings.
92+
account, domain, _, err := win.LookupAccount(tUser.User.Sid, "")
93+
_ = win.CloseToken(t)
94+
if err != nil {
95+
_ = win.CloseHandle(pHandle)
96+
continue
97+
}
98+
99+
// 4. Compare against the expected Hyper-V UVM identity.
100+
if strings.EqualFold(domain, ntVirtualMachineDomain) && strings.EqualFold(account, vmIDStr) {
101+
log.G(ctx).WithField("pid", pe32.ProcessID).Debug("found vmmem match")
102+
return pHandle, nil
103+
}
104+
105+
_ = win.CloseHandle(pHandle)
106+
}
107+
108+
return 0, errors.New("failed to find matching vmmem process")
109+
}

0 commit comments

Comments
 (0)