Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ require (
go.etcd.io/bbolt v1.4.0
go.opencensus.io v0.24.0
go.uber.org/mock v0.6.0
golang.org/x/net v0.43.0
golang.org/x/sync v0.16.0
golang.org/x/sys v0.35.0
google.golang.org/grpc v1.75.0
Expand Down Expand Up @@ -113,7 +114,6 @@ require (
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
Expand Down
4 changes: 4 additions & 0 deletions internal/gcs/guestcaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,7 @@ func (w *WCOWGuestDefinedCapabilities) IsDumpStacksSupported() bool {
func (w *WCOWGuestDefinedCapabilities) IsDeleteContainerStateSupported() bool {
return w.DeleteContainerStateSupported
}

func (w *WCOWGuestDefinedCapabilities) IsLogForwardingSupported() bool {
return w.LogForwardingSupported
}
14 changes: 14 additions & 0 deletions internal/gcs/guestconnection.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,20 @@ func (gc *GuestConnection) Modify(ctx context.Context, settings interface{}) (er
return gc.brdg.RPC(ctx, prot.RPCModifySettings, &req, &resp, false)
}

func (gc *GuestConnection) ModifyServiceSettings(ctx context.Context, serviceType prot.ServiceModifyPropertyType, settings interface{}) (err error) {
ctx, span := oc.StartSpan(ctx, "gcs::GuestConnection::ModifyServiceSettings", oc.WithClientSpanKind)
defer span.End()
defer func() { oc.SetSpanStatus(span, err) }()

req := prot.ServiceModificationRequest{
RequestBase: makeRequest(ctx, nullContainerID),
PropertyType: serviceType.String(),
Settings: settings,
}
var resp prot.ResponseBase
return gc.brdg.RPC(ctx, prot.RPCModifyServiceSettings, &req, &resp, false)
}

func (gc *GuestConnection) DumpStacks(ctx context.Context) (response string, err error) {
ctx, span := oc.StartSpan(ctx, "gcs::GuestConnection::DumpStacks", oc.WithClientSpanKind)
defer span.End()
Expand Down
71 changes: 58 additions & 13 deletions internal/gcs/prot/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ var WindowsGcsHvHostID = guid.GUID{
Data4: [8]uint8{0x93, 0xfe, 0x42, 0x96, 0x9a, 0xe6, 0xd8, 0xd1},
}

// WindowsLoggingHvsockServiceID is the hvsock service ID that the Windows log forward service
// will connect to. 172dad59-976d-45f2-8b6c-6d1b13f2ac4d
var WindowsLoggingHvsockServiceID = guid.GUID{
Data1: 0x172dad59,
Data2: 0x976d,
Data3: 0x45f2,
Data4: [8]uint8{0x8b, 0x6c, 0x6d, 0x1b, 0x13, 0xf2, 0xac, 0x4d},
}

type AnyInString struct {
Value interface{}
}
Expand All @@ -83,10 +92,17 @@ func (a *AnyInString) UnmarshalText(b []byte) error {
return json.Unmarshal(b, &a.Value)
}

const (
// Message_Category for the GCS protocol.
ComputeSystem = 0x00100000
ComputeService = 0x00200000
)

type RPCProc uint32

const (
RPCCreate RPCProc = (iota+1)<<8 | 1
// Compute System RPCs
RPCCreate RPCProc = ComputeSystem | (iota+1)<<8 | 1
RPCStart
RPCShutdownGraceful
RPCShutdownForced
Expand All @@ -103,6 +119,26 @@ const (
RPCLifecycleNotification
)

const (
// Compute Service RPCs
RPCModifyServiceSettings RPCProc = ComputeService | (iota+1)<<8 | 1
)

type ServiceModifyPropertyType uint32

const (
LogForwardService ServiceModifyPropertyType = iota
)

func (m ServiceModifyPropertyType) String() string {
switch m {
case LogForwardService:
return "LogForwardService"
default:
return fmt.Sprintf("UnknownModifyServiceType(%d)", m)
}
}
Comment on lines +127 to +140
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not have ServiceModifyPropertyType be a string, similar to the PropertyType and RequestType enums, since we don't ever need to use the integer value?\

type ServiceModifyPropertyType string

const (
	LogForwardService = ServiceModifyPropertyType("LogForwardService")
)

// ...

type ServiceModificationRequest struct {
	RequestBase
	PropertyType ServiceModifyPropertyType `json:",omitempty"`
	Settings     interface{}               `json:",omitempty"`
}


func (rpc RPCProc) String() string {
switch rpc {
case RPCCreate:
Expand Down Expand Up @@ -135,6 +171,8 @@ func (rpc RPCProc) String() string {
return "UpdateContainer"
case RPCLifecycleNotification:
return "LifecycleNotification"
case RPCModifyServiceSettings:
return "ModifyServiceSettings"
default:
return "0x" + strconv.FormatUint(uint64(rpc), 16)
}
Expand All @@ -143,10 +181,10 @@ func (rpc RPCProc) String() string {
type MsgType uint32

const (
MsgTypeRequest MsgType = 0x10100000
MsgTypeResponse MsgType = 0x20100000
MsgTypeNotify MsgType = 0x30100000
MsgTypeMask MsgType = 0xfff00000
MsgTypeRequest MsgType = 0x10000000
MsgTypeResponse MsgType = 0x20000000
MsgTypeNotify MsgType = 0x30000000
MsgTypeMask MsgType = 0xf0000000
Comment on lines +184 to +187
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think you also need to update bridge and test code with the new constants (not sure where else will need to be changed)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!
Though this makes me re-think why only NotifyContainer is implemented in this unique way by creating the MsgType manually instead of utilizing the standard proc.MsgType(RPCLifecycleNotification) like others. Manually creating the messages in multiple places only adds to potential bugs in case of changes


NotifyContainer = 1<<8 | 1
)
Expand All @@ -160,7 +198,7 @@ func (typ MsgType) String() string {
s = "Response("
case MsgTypeNotify:
s = "Notify("
switch typ - MsgTypeNotify {
switch typ - (ComputeSystem | MsgTypeNotify) {
case NotifyContainer:
s += "Container"
default:
Expand Down Expand Up @@ -267,6 +305,12 @@ type ContainerNotification struct {
ResultInfo AnyInString `json:",omitempty"`
}

type ServiceModificationRequest struct {
RequestBase
PropertyType string // ServiceModifyPropertyType
Settings interface{} `json:",omitempty"`
}

type ContainerExecuteProcess struct {
RequestBase
Settings ExecuteProcessSettings
Expand Down Expand Up @@ -345,13 +389,14 @@ type ContainerModifySettings struct {
}

type GcsCapabilities struct {
SendHostCreateMessage bool
SendHostStartMessage bool
HvSocketConfigOnStartup bool
SendLifecycleNotifications bool
SupportedSchemaVersions []hcsschema.Version
RuntimeOsType string
GuestDefinedCapabilities json.RawMessage
SendHostCreateMessage bool
SendHostStartMessage bool
HvSocketConfigOnStartup bool
SendLifecycleNotifications bool
ModifyServiceSettingsSupported bool
SupportedSchemaVersions []hcsschema.Version
RuntimeOsType string
GuestDefinedCapabilities json.RawMessage
}

type ContainerCreateResponse struct {
Expand Down
1 change: 1 addition & 0 deletions internal/hcs/schema1/schema1.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ type GuestDefinedCapabilities struct {
DumpStacksSupported bool `json:",omitempty"`
DeleteContainerStateSupported bool `json:",omitempty"`
UpdateContainerSupported bool `json:",omitempty"`
LogForwardingSupported bool `json:",omitempty"`
}

// GuestConnectionInfo is the structure of an iterm return by a GuestConnection call on a utility VM
Expand Down
7 changes: 6 additions & 1 deletion internal/oci/uvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,12 @@ func SpecToUVMCreateOpts(ctx context.Context, s *specs.Spec, id, owner string) (
if err := handleWCOWSecurityPolicy(ctx, s.Annotations, wopts); err != nil {
return nil, err
}

// If security policy is enable, wopts.ForwardLogs default value should be false
if wopts.SecurityPolicyEnabled {
wopts.ForwardLogs = false
}
wopts.LogSources = ParseAnnotationsString(s.Annotations, annotations.LogSources, wopts.LogSources)
wopts.ForwardLogs = ParseAnnotationsBool(ctx, s.Annotations, annotations.ForwardLogs, wopts.ForwardLogs)
return wopts, nil
}
return nil, errors.New("cannot create UVM opts spec is not LCOW or WCOW")
Expand Down
4 changes: 4 additions & 0 deletions internal/oci/uvm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ func Test_SpecToUVMCreateOptions_Default_WCOW(t *testing.T) {
wopts := (opts).(*uvm.OptionsWCOW)
dopts := uvm.NewDefaultOptionsWCOW(t.Name(), "")

// output handler equality is always false, so set to nil
wopts.OutputHandlerCreator = nil
dopts.OutputHandlerCreator = nil

if !cmp.Equal(*wopts, *dopts) {
t.Fatalf("should not have updated create options from default when no annotation are provided:\n%s", cmp.Diff(wopts, dopts))
}
Expand Down
14 changes: 14 additions & 0 deletions internal/protocol/guestrequest/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,17 @@ const (
STDErrHandle STDIOHandle = "StdErr"
AllHandles STDIOHandle = "All"
)

type LogForwardServiceRPCRequest struct {
RPCType RPCType `json:"RPCType,omitempty"` // "LogForwardService"
Settings string `json:"Settings,omitempty"`
}

type RPCType string

const (
// LogForwardServiceRPC is the RPC type for the log forward service.
RPCModifyServiceSettings RPCType = "ModifyServiceSettings"
RPCStartLogForwarding RPCType = "StartLogForwarding"
RPCStopLogForwarding RPCType = "StopLogForwarding"
)
24 changes: 24 additions & 0 deletions internal/uvm/create_wcow.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ type OptionsWCOW struct {

// AdditionalRegistryKeys are Registry keys and their values to additionally add to the uVM.
AdditionalRegistryKeys []hcsschema.RegistryValue

OutputHandlerCreator OutputHandlerCreator // Creates an [OutputHandler] that controls how output received over HVSocket from the UVM is handled. Defaults to parsing output as ETW Log events
LogSources string // ETW providers to be set for the logging service
ForwardLogs bool // Whether to forward logs to the host or not
}

func defaultConfidentialWCOWOSBootFilesPath() string {
Expand Down Expand Up @@ -89,6 +93,9 @@ func NewDefaultOptionsWCOW(id, owner string) *OptionsWCOW {
Options: newDefaultOptions(id, owner),
AdditionalRegistryKeys: []hcsschema.RegistryValue{},
ConfidentialWCOWOptions: &ConfidentialWCOWOptions{},
OutputHandlerCreator: parseLogrus,
ForwardLogs: true, // Default to true for WCOW, and set to false for CWCOW in internal/oci/uvm.go SpecToUVMCreateOpts
LogSources: "",
}
}

Expand Down Expand Up @@ -259,6 +266,12 @@ func prepareCommonConfigDoc(ctx context.Context, uvm *UtilityVM, opts *OptionsWC
}

maps.Copy(doc.VirtualMachine.Devices.HvSocket.HvSocketConfig.ServiceTable, opts.AdditionalHyperVConfig)
key := prot.WindowsLoggingHvsockServiceID.String()
doc.VirtualMachine.Devices.HvSocket.HvSocketConfig.ServiceTable[key] = hcsschema.HvSocketServiceConfig{
AllowWildcardBinds: true,
BindSecurityDescriptor: "D:P(A;;FA;;;SY)(A;;FA;;;BA)",
ConnectSecurityDescriptor: "D:P(A;;FA;;;SY)(A;;FA;;;BA)",
}
Comment on lines +269 to +274
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to CreateWCOW, can we skip this if !uvm.forwardLogs?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a proposal to have option of being able to enable log forwarding by uvm update, but not implemented yet. Keeping that in mind, we would prefer this hvsocket being always there even though initially forwardlogs could be false at the time of creation


// Handle StorageQoS if set
if opts.StorageQoSBandwidthMaximum > 0 || opts.StorageQoSIopsMaximum > 0 {
Expand Down Expand Up @@ -507,6 +520,8 @@ func CreateWCOW(ctx context.Context, opts *OptionsWCOW) (_ *UtilityVM, err error
noWritableFileShares: opts.NoWritableFileShares,
createOpts: *opts,
blockCIMMounts: make(map[string]*UVMMountedBlockCIMs),
logSources: opts.LogSources,
forwardLogs: opts.ForwardLogs,
}

defer func() {
Expand Down Expand Up @@ -536,6 +551,15 @@ func CreateWCOW(ctx context.Context, opts *OptionsWCOW) (_ *UtilityVM, err error
return nil, fmt.Errorf("error while creating the compute system: %w", err)
}

// Create a socket that the executed program can send to. This is usually
// used by Log Forward Service to send log data.
uvm.outputHandler = opts.OutputHandlerCreator(opts.Options)
uvm.outputProcessingDone = make(chan struct{})
uvm.outputListener, err = winio.ListenHvsock(&winio.HvsockAddr{
VMID: uvm.RuntimeID(),
ServiceID: prot.WindowsLoggingHvsockServiceID,
})
Comment on lines +556 to +561
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we gate this on uvm.forwardLogs, so we can skip starting the output handler if we're not forwarding logs

Copy link
Author

@marma-dev marma-dev Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a proposal to have option of being able to enable log forwarding by uvm update, but not implemented yet. Keeping that in mind, we would prefer this outputhandler being always there even though initially forwardlogs could be false at the time of creation as log service could start forwarding logs later on


gcsServiceID := prot.WindowsGcsHvsockServiceID
if opts.SecurityPolicyEnabled {
gcsServiceID = prot.WindowsSidecarGcsHvsockServiceID
Expand Down
69 changes: 69 additions & 0 deletions internal/uvm/log_wcow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//go:build windows

package uvm

import (
"context"

"github.com/Microsoft/hcsshim/internal/gcs"
"github.com/Microsoft/hcsshim/internal/gcs/prot"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/protocol/guestrequest"
)

func (uvm *UtilityVM) StartLogForwarding(ctx context.Context) error {
// Implementation for stopping the log forwarder
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Implementation for stopping the log forwarder
// Implementation for starting the log forwarder

if uvm.OS() == "windows" && uvm.gc != nil {
wcaps := gcs.GetWCOWCapabilities(uvm.gc.Capabilities())
if wcaps != nil && wcaps.IsLogForwardingSupported() {
req := guestrequest.LogForwardServiceRPCRequest{
RPCType: guestrequest.RPCStartLogForwarding,
Settings: "",
}
err := uvm.gc.ModifyServiceSettings(ctx, prot.LogForwardService, req)
if err != nil {
return err
}
} else {
log.G(ctx).WithField("os", uvm.operatingSystem).Error("Log forwarding not supported for this OS")
}
}
return nil
Comment on lines +16 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if uvm.OS() == "windows" && uvm.gc != nil {
wcaps := gcs.GetWCOWCapabilities(uvm.gc.Capabilities())
if wcaps != nil && wcaps.IsLogForwardingSupported() {
req := guestrequest.LogForwardServiceRPCRequest{
RPCType: guestrequest.RPCStartLogForwarding,
Settings: "",
}
err := uvm.gc.ModifyServiceSettings(ctx, prot.LogForwardService, req)
if err != nil {
return err
}
} else {
log.G(ctx).WithField("os", uvm.operatingSystem).Error("Log forwarding not supported for this OS")
}
}
return nil
if uvm.OS() != "windows" || uvm.gc == nil {
return nil
}
wcaps := gcs.GetWCOWCapabilities(uvm.gc.Capabilities())
if wcaps != nil && wcaps.IsLogForwardingSupported() {
req := guestrequest.LogForwardServiceRPCRequest{
RPCType: guestrequest.RPCStartLogForwarding,
Settings: "",
}
err := uvm.gc.ModifyServiceSettings(ctx, prot.LogForwardService, req)
if err != nil {
return err
}
} else {
log.G(ctx).WithField("os", uvm.operatingSystem).Error("Log forwarding not supported for this OS")
}

}

func (uvm *UtilityVM) StopLogForwarding(ctx context.Context) error {
// Implementation for stopping the log forwarder
if uvm.OS() == "windows" && uvm.gc != nil {
wcaps := gcs.GetWCOWCapabilities(uvm.gc.Capabilities())
if wcaps != nil && wcaps.IsLogForwardingSupported() {
req := guestrequest.LogForwardServiceRPCRequest{
RPCType: guestrequest.RPCStopLogForwarding,
Settings: "",
}
err := uvm.gc.ModifyServiceSettings(ctx, prot.LogForwardService, req)
if err != nil {
return err
}
}
}
return nil
}

func (uvm *UtilityVM) SetLogSources(ctx context.Context) error {
// Implementation for setting the log sources
if uvm.OS() == "windows" && uvm.logSources != "" && uvm.gc != nil {
wcaps := gcs.GetWCOWCapabilities(uvm.gc.Capabilities())
if wcaps != nil && wcaps.IsLogForwardingSupported() {
// Make a call to the GCS to set the ETW providers
req := guestrequest.LogForwardServiceRPCRequest{
RPCType: guestrequest.RPCModifyServiceSettings,
Settings: uvm.logSources,
}
err := uvm.gc.ModifyServiceSettings(ctx, prot.LogForwardService, req)
if err != nil {
return err
}
}
}
return nil
}
Loading
Loading