internal/resource/resource_windows.go (125 lines of code) (raw):
// Copyright 2024 Google LLC
//
// 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
//
// https://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,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package resource
import (
"context"
"fmt"
"os"
"syscall"
"time"
"unsafe"
"github.com/GoogleCloudPlatform/galog"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/events"
"golang.org/x/sys/windows"
)
// windowsClient is a client for applying resource constraints on Windows.
// This contains the functions that are stubbed out for error-injection purposes.
type windowsClient struct {
createJobObject func(jobAttr *windows.SecurityAttributes, name *uint16) (windows.Handle, error)
getProcessHandle func(access uint32, inheritHandle bool, pid uint32) (windows.Handle, error)
assignProcessToJobObject func(jobHandle windows.Handle, processHandle windows.Handle) error
setInformationJobObject func(jobHandle windows.Handle, infoClass uint32, info uintptr, infoLen uint32) (int, error)
handles map[string]windows.Handle
}
// jobObjectCPURateControlInfo is used for CPU constraints in Windows JobObjects.
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information
type jobObjectCPURateControlInfo struct {
ControlFlag uint32
Value uint32
}
const (
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information#members
jobObjectCPURateControlEnable uint32 = 0x1
jobObjectCPUHardCap uint32 = 0x4
// https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights
// This provides permission to do everything to the process with the handle.
processAllAccess = 0x1FFFFF
)
// init initializes the windowsClient.
func init() {
Client = &windowsClient{
createJobObject: windows.CreateJobObject,
getProcessHandle: windows.OpenProcess,
assignProcessToJobObject: windows.AssignProcessToJobObject,
setInformationJobObject: windows.SetInformationJobObject,
handles: make(map[string]windows.Handle),
}
}
// Apply applies resource constraints to the process with the given PID.
// For Windows, this uses JobObjects, since they can command both memory and cpu
// restrictions.
//
// Modifying and performing actions on JobObjects require access to the handles,
// which we obtain whenever we create a JobObject. However, unless all handles
// to a JobObject are closed, the JobObject will continue to persist. The windows
// functions `OpenJobObject` and `CreateJobObject` return new handles to the
// JobObject instead of fetching the existing one. This means that unless the
// original handle returned by the creation of the JobObject is saved somewhere,
// there are no means with which the agent can close those handles and clean up
// the JobObject. To solve this issue, a handle map is used to internally cache
// the handles to JobObjects that are currently in use.
//
// It's important to note that if the guest-agent is stopped for any reason,
// all the JobObjects created by the guest-agent will be destroyed, meaning
// that the processes will run unconstrained until the guest-agent is restarted
// and can re-apply the constraints. However, we believe this should not pose a
// significant problem because we assume the guest agent is running at all
// times, with a few exceptions such as package updates. This also means that
// there is no need to persist the handles map through restarts because new
// JobObjects will be created to reapply the resource constraints to the
// processes after a restart.
func (c *windowsClient) Apply(constraint Constraint) error {
// If no constraints are set, return early.
if constraint.MaxMemoryUsage == 0 && constraint.MaxCPUUsage == 0 {
return nil
}
// Create a JobObject for this plugin.
jobName, err := windows.UTF16PtrFromString(constraint.Name)
if err != nil {
return fmt.Errorf("failed to parse plugin name: %w", err)
}
jobObjectHandle, err := c.createJobObject(nil, jobName)
if err != nil && err != windows.ERROR_ALREADY_EXISTS {
return fmt.Errorf("error creating job object for plugin %s: %w", constraint.Name, err)
}
c.handles[constraint.Name] = jobObjectHandle
galog.Debugf("Created JobObject for plugin %s with handle %v", constraint.Name, jobObjectHandle)
// Get the process handle.
procHandle, err := c.getProcessHandle(processAllAccess, false, uint32(constraint.PID))
if err != nil {
return fmt.Errorf("failed to open process handler for plugin %s: %w", constraint.Name, err)
}
defer windows.CloseHandle(procHandle)
// Assign the process to the JobObject.
if err := c.assignProcessToJobObject(jobObjectHandle, procHandle); err != nil {
return fmt.Errorf("failed to assign process %v to job object: %w", constraint.Name, err)
}
// Set the process memory limits.
if constraint.MaxMemoryUsage != 0 {
// First validate that the MaxMemoryUsage is greater than the minimum
// required, which is 20 pages.
//
// https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-setprocessworkingsetsizeex
minimumSize := 20 * os.Getpagesize()
if constraint.MaxMemoryUsage < int64(minimumSize) {
return fmt.Errorf("MaxMemoryUsage %d is less than the minimum required memory %d", constraint.MaxMemoryUsage, minimumSize)
}
jobMemoryLimitInfoClass := uint32(windows.JobObjectBasicLimitInformation)
jobLimitInfo := windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{
LimitFlags: windows.JOB_OBJECT_LIMIT_WORKINGSET,
MinimumWorkingSetSize: uintptr(minimumSize),
MaximumWorkingSetSize: uintptr(constraint.MaxMemoryUsage),
}
if _, err = c.setInformationJobObject(
jobObjectHandle,
jobMemoryLimitInfoClass,
uintptr(unsafe.Pointer(&jobLimitInfo)),
uint32(unsafe.Sizeof(jobLimitInfo)),
); err != nil {
return fmt.Errorf("failed to set job object memory limits for plugin %s: %w", constraint.Name, err)
}
}
if constraint.MaxCPUUsage != 0 {
// Make sure the CPU usage set is realistic.
if constraint.MaxCPUUsage > 100 {
return fmt.Errorf("MaxCPUUsage %d exceeds 100%%", constraint.MaxCPUUsage)
}
// Set CPU rate limits. Usage limits is the maximum number of CPU cycles
// allowed per 10,000 cycles.
cpuCycles := uint32(constraint.MaxCPUUsage * 100)
jobCPURateLimitInfoClass := uint32(windows.JobObjectCpuRateControlInformation)
jobCPURateControl := jobObjectCPURateControlInfo{
ControlFlag: jobObjectCPUHardCap | jobObjectCPURateControlEnable,
Value: cpuCycles,
}
if _, err = c.setInformationJobObject(
jobObjectHandle,
jobCPURateLimitInfoClass,
uintptr(unsafe.Pointer(&jobCPURateControl)),
uint32(unsafe.Sizeof(jobCPURateControl)),
); err != nil {
return fmt.Errorf("failed to set CPU rate limits for plugin %s: %w", constraint.Name, err)
}
}
return nil
}
// RemoveConstraint removes the constraint from the process defined by the given
// name. For Windows, this merely closes the original handle to the JobObject
// defined by the given name.
func (c *windowsClient) RemoveConstraint(ctx context.Context, name string) error {
// Obtain the handle from map.
handle, found := c.handles[name]
if !found {
galog.Debugf("No JobObject found for plugin %s", name)
return nil
}
// Closing the handle destroys the JobObject. This should be the only handle
// pointing to the JobObject.
galog.Debugf("Removing JobObject (handle %v) for plugin %s", handle, name)
err := windows.CloseHandle(handle)
if err != nil {
return fmt.Errorf("failed to close JobObject handle for plugin %s: %w", name, err)
}
// Remove the mapping after successfully closing the handle.
delete(c.handles, name)
return nil
}
// NewOOMWatcher registers an OOM event watcher for the given plugin.
func (windowsClient) NewOOMWatcher(ctx context.Context, constraint Constraint, interval time.Duration) (events.Watcher, error) {
watcher := &oomWatcher{
name: constraint.Name,
pid: constraint.PID,
maxMemory: constraint.MaxMemoryUsage,
interval: interval,
openProcess: windows.OpenProcess,
syscall: syscall.SyscallN,
}
return watcher, events.FetchManager().AddWatcher(ctx, watcher)
}