From 448ef713ac71fdd833129eb0cea584005d2ecb1f Mon Sep 17 00:00:00 2001 From: Harsh Rawat Date: Sat, 21 Mar 2026 17:01:06 +0530 Subject: [PATCH 1/2] [shimV2] add plan9 device controller This change adds the plan9 device controller which can add/remove plan9 shares from a VM. The guest side operations are part of mount controller responsibility. Signed-off-by: Harsh Rawat --- internal/controller/device/plan9/doc.go | 21 +++ internal/controller/device/plan9/interface.go | 44 ++++++ internal/controller/device/plan9/plan9.go | 142 ++++++++++++++++++ internal/vm/vmmanager/plan9.go | 11 -- 4 files changed, 207 insertions(+), 11 deletions(-) create mode 100644 internal/controller/device/plan9/doc.go create mode 100644 internal/controller/device/plan9/interface.go create mode 100644 internal/controller/device/plan9/plan9.go diff --git a/internal/controller/device/plan9/doc.go b/internal/controller/device/plan9/doc.go new file mode 100644 index 0000000000..f61709877d --- /dev/null +++ b/internal/controller/device/plan9/doc.go @@ -0,0 +1,21 @@ +//go:build windows && !wcow + +// Package plan9 provides a controller for managing Plan9 file-share devices +// attached to a Utility VM (UVM). +// +// It handles attaching and detaching Plan9 shares to the VM via HCS modify +// calls. Guest-side mount operations (mapped-directory requests) are handled +// separately by the mount controller. +// +// The [Controller] interface is the primary entry point, with [Manager] as its +// concrete implementation. A single [Manager] manages all Plan9 shares for a UVM. +// +// # Lifecycle +// +// [Manager] tracks active shares by name in an internal map. +// +// - [Controller.AddToVM] adds a share and records its name in the map. +// If the HCS call fails, the share is not recorded. +// - [Controller.RemoveFromVM] removes a share and deletes its name from the map. +// If the share is not in the map, the call is a no-op. +package plan9 diff --git a/internal/controller/device/plan9/interface.go b/internal/controller/device/plan9/interface.go new file mode 100644 index 0000000000..d1c5e78e39 --- /dev/null +++ b/internal/controller/device/plan9/interface.go @@ -0,0 +1,44 @@ +//go:build windows && !wcow + +package plan9 + +import ( + "context" + + hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" +) + +// Controller manages the lifecycle of Plan9 shares attached to a UVM. +type Controller interface { + // AddToVM adds a Plan9 share to the host VM and returns the generated share name. + // Guest-side mount is handled separately by the mount controller. + AddToVM(ctx context.Context, opts *AddOptions) (string, error) + + // RemoveFromVM removes a Plan9 share identified by shareName from the host VM. + RemoveFromVM(ctx context.Context, shareName string) error +} + +// AddOptions holds the configuration required to add a Plan9 share to the VM. +type AddOptions struct { + // HostPath is the path on the host to share into the VM. + HostPath string + + // ReadOnly indicates whether the share should be mounted read-only. + ReadOnly bool + + // Restrict enables single-file mapping mode for the share. + Restrict bool + + // AllowedNames is the list of file names allowed when Restrict is true. + AllowedNames []string +} + +// vmPlan9Manager manages adding and removing Plan9 shares on the host VM. +// Implemented by [vmmanager.UtilityVM]. +type vmPlan9Manager interface { + // AddPlan9 adds a plan 9 share to a running Utility VM. + AddPlan9(ctx context.Context, settings hcsschema.Plan9Share) error + + // RemovePlan9 removes a plan 9 share from a running Utility VM. + RemovePlan9(ctx context.Context, settings hcsschema.Plan9Share) error +} diff --git a/internal/controller/device/plan9/plan9.go b/internal/controller/device/plan9/plan9.go new file mode 100644 index 0000000000..33749eea63 --- /dev/null +++ b/internal/controller/device/plan9/plan9.go @@ -0,0 +1,142 @@ +//go:build windows && !wcow + +package plan9 + +import ( + "context" + "fmt" + "strconv" + "sync" + + "github.com/Microsoft/hcsshim/internal/hcs" + hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" + "github.com/Microsoft/hcsshim/internal/log" + "github.com/Microsoft/hcsshim/internal/logfields" + "github.com/Microsoft/hcsshim/internal/vm/vmutils" + + "github.com/sirupsen/logrus" +) + +// share-flag constants used in Plan9 HCS requests. +// +// These are marked private in the HCS schema. When public variants become +// available, we should replace these. +const ( + shareFlagsReadOnly int32 = 0x00000001 + shareFlagsLinuxMetadata int32 = 0x00000004 + shareFlagsRestrictFileAccess int32 = 0x00000080 +) + +// Manager is the concrete implementation of [Controller]. +type Manager struct { + mu sync.Mutex + + // shares is the set of currently configured Plan9 share names. + // Guarded by mu. + shares map[string]struct{} + + // noWritableFileShares disallows adding writable Plan9 shares. + noWritableFileShares bool + + // vmPlan9Mgr performs host-side Plan9 add/remove on the VM. + vmPlan9Mgr vmPlan9Manager + + // nameCounter is the monotonically increasing index used to + // generate unique share names. Guarded by mu. + nameCounter uint64 +} + +var _ Controller = (*Manager)(nil) + +// New creates a ready-to-use [Manager]. +func New( + vmPlan9Mgr vmPlan9Manager, + noWritableFileShares bool, +) *Manager { + return &Manager{ + vmPlan9Mgr: vmPlan9Mgr, + noWritableFileShares: noWritableFileShares, + shares: make(map[string]struct{}), + } +} + +// AddToVM adds a Plan9 share to the host VM and returns the generated share name. +func (m *Manager) AddToVM(ctx context.Context, opts *AddOptions) (_ string, err error) { + m.mu.Lock() + defer m.mu.Unlock() + + // Ensure that adding the share is allowed. + if !opts.ReadOnly && m.noWritableFileShares { + return "", fmt.Errorf("adding writable shares is denied: %w", hcs.ErrOperationDenied) + } + + // Build the Plan9 share flags bitmask from the caller-provided options. + flags := shareFlagsLinuxMetadata + if opts.ReadOnly { + flags |= shareFlagsReadOnly + } + if opts.Restrict { + flags |= shareFlagsRestrictFileAccess + } + + // Generate a unique share name from the nameCounter. + name := strconv.FormatUint(m.nameCounter, 10) + m.nameCounter++ + + ctx, _ = log.WithContext(ctx, logrus.WithField("shareName", name)) + + log.G(ctx).WithFields(logrus.Fields{ + logfields.HostPath: opts.HostPath, + logfields.ReadOnly: opts.ReadOnly, + "RestrictFileAccess": opts.Restrict, + "AllowedFiles": opts.AllowedNames, + }).Tracef("adding plan9 share to host VM") + + // Call into HCS to add the Plan9 share to the VM. + if err := m.vmPlan9Mgr.AddPlan9(ctx, hcsschema.Plan9Share{ + Name: name, + AccessName: name, + Path: opts.HostPath, + Port: vmutils.Plan9Port, + Flags: flags, + AllowedFiles: opts.AllowedNames, + }); err != nil { + return "", fmt.Errorf("add plan9 share %s to host: %w", name, err) + } + + m.shares[name] = struct{}{} + + log.G(ctx).Info("plan9 share added to host VM") + + return name, nil +} + +// RemoveFromVM removes the Plan9 share identified by shareName from the host VM. +func (m *Manager) RemoveFromVM(ctx context.Context, shareName string) error { + m.mu.Lock() + defer m.mu.Unlock() + + ctx, _ = log.WithContext(ctx, logrus.WithField("shareName", shareName)) + + if _, ok := m.shares[shareName]; !ok { + log.G(ctx).Debug("plan9 share not found, skipping removal") + return nil + } + + log.G(ctx).Debug("starting plan9 share removal") + + // Call into HCS to remove the share from the VM. + if err := m.vmPlan9Mgr.RemovePlan9(ctx, hcsschema.Plan9Share{ + Name: shareName, + AccessName: shareName, + Port: vmutils.Plan9Port, + }); err != nil { + return fmt.Errorf("remove plan9 share %s from host: %w", shareName, err) + } + + delete(m.shares, shareName) + + log.G(ctx).Info("plan9 share removed from host VM") + + return nil +} diff --git a/internal/vm/vmmanager/plan9.go b/internal/vm/vmmanager/plan9.go index 3effcf8ac2..9222268477 100644 --- a/internal/vm/vmmanager/plan9.go +++ b/internal/vm/vmmanager/plan9.go @@ -11,17 +11,6 @@ import ( "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" ) -// Plan9Manager manages adding plan 9 shares to a Utility VM. -type Plan9Manager interface { - // AddPlan9 adds a plan 9 share to a running Utility VM. - AddPlan9(ctx context.Context, settings hcsschema.Plan9Share) error - - // RemovePlan9 removes a plan 9 share from a running Utility VM. - RemovePlan9(ctx context.Context, settings hcsschema.Plan9Share) error -} - -var _ Plan9Manager = (*UtilityVM)(nil) - func (uvm *UtilityVM) AddPlan9(ctx context.Context, settings hcsschema.Plan9Share) error { modification := &hcsschema.ModifySettingRequest{ RequestType: guestrequest.RequestTypeAdd, From 2ecafc6a26df4e5736769a8565fbf8a8e5168296 Mon Sep 17 00:00:00 2001 From: Harsh Rawat Date: Wed, 25 Mar 2026 20:54:27 +0530 Subject: [PATCH 2/2] review comments: 1 Signed-off-by: Harsh Rawat --- internal/controller/device/plan9/doc.go | 61 ++++- internal/controller/device/plan9/interface.go | 44 ---- internal/controller/device/plan9/plan9.go | 228 +++++++++++++----- internal/controller/device/plan9/state.go | 55 +++++ internal/controller/device/plan9/types.go | 85 +++++++ 5 files changed, 360 insertions(+), 113 deletions(-) delete mode 100644 internal/controller/device/plan9/interface.go create mode 100644 internal/controller/device/plan9/state.go create mode 100644 internal/controller/device/plan9/types.go diff --git a/internal/controller/device/plan9/doc.go b/internal/controller/device/plan9/doc.go index f61709877d..08c2ddc9b7 100644 --- a/internal/controller/device/plan9/doc.go +++ b/internal/controller/device/plan9/doc.go @@ -1,21 +1,62 @@ //go:build windows && !wcow -// Package plan9 provides a controller for managing Plan9 file-share devices +// Package plan9 provides a manager for managing Plan9 file-share devices // attached to a Utility VM (UVM). // -// It handles attaching and detaching Plan9 shares to the VM via HCS modify +// It handles adding and removing Plan9 shares on the host side via HCS modify // calls. Guest-side mount operations (mapped-directory requests) are handled -// separately by the mount controller. +// separately by the mount manager. // -// The [Controller] interface is the primary entry point, with [Manager] as its -// concrete implementation. A single [Manager] manages all Plan9 shares for a UVM. +// # Deduplication and Reference Counting +// +// [Manager] deduplicates shares: if two callers add a share with identical +// [AddOptions], the second call reuses the existing share and increments an +// internal reference count rather than issuing a second HCS call. The share is +// only removed from the VM when the last caller invokes [Manager.RemoveFromVM]. // // # Lifecycle // -// [Manager] tracks active shares by name in an internal map. +// Each share progresses through the states below. +// The happy path runs down the left column; the error path is on the right. +// +// Allocate entry for the share +// │ +// ▼ +// ┌─────────────────────┐ +// │ sharePending │ +// └──────────┬──────────┘ +// │ +// ┌───────┴────────────────────────────────┐ +// │ AddPlan9 succeeds │ AddPlan9 fails +// ▼ ▼ +// ┌─────────────────────┐ ┌──────────────────────┐ +// │ shareAdded │ │ shareInvalid │ +// └──────────┬──────────┘ └──────────────────────┘ +// │ RemovePlan9 succeeds (auto-removed from map) +// ▼ +// ┌─────────────────────┐ +// │ shareRemoved │ ← terminal; entry removed from map +// └─────────────────────┘ +// +// State descriptions: +// +// - [sharePending]: entered when a new entry is allocated (by [Manager.ResolveShareName] +// or the first [Manager.AddToVM] call). No HCS call has been made yet. +// - [shareAdded]: entered once [vmPlan9Manager.AddPlan9] succeeds; +// the share is live on the VM. +// - [shareInvalid]: entered when [vmPlan9Manager.AddPlan9] fails; +// the map entry is removed immediately so the next call can retry. +// - [shareRemoved]: terminal state entered once [vmPlan9Manager.RemovePlan9] succeeds. +// +// Method summary: // -// - [Controller.AddToVM] adds a share and records its name in the map. -// If the HCS call fails, the share is not recorded. -// - [Controller.RemoveFromVM] removes a share and deletes its name from the map. -// If the share is not in the map, the call is a no-op. +// - [Manager.ResolveShareName] pre-allocates a share name for the given [AddOptions] +// without issuing any HCS call. If a matching share is already tracked, +// the existing name is returned. This is useful for resolving downstream +// resource paths (e.g., guest mount paths) before the share is live. +// - [Manager.AddToVM] attaches the share, driving the HCS AddPlan9 call on +// the first caller and incrementing the reference count on subsequent ones. +// If the HCS call fails, the entry is removed so the next call can retry. +// - [Manager.RemoveFromVM] decrements the reference count and tears down the +// share only when the count reaches zero. package plan9 diff --git a/internal/controller/device/plan9/interface.go b/internal/controller/device/plan9/interface.go deleted file mode 100644 index d1c5e78e39..0000000000 --- a/internal/controller/device/plan9/interface.go +++ /dev/null @@ -1,44 +0,0 @@ -//go:build windows && !wcow - -package plan9 - -import ( - "context" - - hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" -) - -// Controller manages the lifecycle of Plan9 shares attached to a UVM. -type Controller interface { - // AddToVM adds a Plan9 share to the host VM and returns the generated share name. - // Guest-side mount is handled separately by the mount controller. - AddToVM(ctx context.Context, opts *AddOptions) (string, error) - - // RemoveFromVM removes a Plan9 share identified by shareName from the host VM. - RemoveFromVM(ctx context.Context, shareName string) error -} - -// AddOptions holds the configuration required to add a Plan9 share to the VM. -type AddOptions struct { - // HostPath is the path on the host to share into the VM. - HostPath string - - // ReadOnly indicates whether the share should be mounted read-only. - ReadOnly bool - - // Restrict enables single-file mapping mode for the share. - Restrict bool - - // AllowedNames is the list of file names allowed when Restrict is true. - AllowedNames []string -} - -// vmPlan9Manager manages adding and removing Plan9 shares on the host VM. -// Implemented by [vmmanager.UtilityVM]. -type vmPlan9Manager interface { - // AddPlan9 adds a plan 9 share to a running Utility VM. - AddPlan9(ctx context.Context, settings hcsschema.Plan9Share) error - - // RemovePlan9 removes a plan 9 share from a running Utility VM. - RemovePlan9(ctx context.Context, settings hcsschema.Plan9Share) error -} diff --git a/internal/controller/device/plan9/plan9.go b/internal/controller/device/plan9/plan9.go index 33749eea63..a8b7cc6210 100644 --- a/internal/controller/device/plan9/plan9.go +++ b/internal/controller/device/plan9/plan9.go @@ -27,13 +27,14 @@ const ( shareFlagsRestrictFileAccess int32 = 0x00000080 ) -// Manager is the concrete implementation of [Controller]. +// Manager is the concrete implementation which manages plan9 shares to the UVM. type Manager struct { + // mu protects the shares map and serializes name allocation across concurrent callers. mu sync.Mutex - // shares is the set of currently configured Plan9 share names. - // Guarded by mu. - shares map[string]struct{} + // shares maps share name → shareEntry for every active or pending share. + // Access must be guarded by mu. + shares map[string]*shareEntry // noWritableFileShares disallows adding writable Plan9 shares. noWritableFileShares bool @@ -41,13 +42,11 @@ type Manager struct { // vmPlan9Mgr performs host-side Plan9 add/remove on the VM. vmPlan9Mgr vmPlan9Manager - // nameCounter is the monotonically increasing index used to - // generate unique share names. Guarded by mu. + // nameCounter is the monotonically increasing index used to generate unique share names. + // Access must be guarded by mu. nameCounter uint64 } -var _ Controller = (*Manager)(nil) - // New creates a ready-to-use [Manager]. func New( vmPlan9Mgr vmPlan9Manager, @@ -56,87 +55,198 @@ func New( return &Manager{ vmPlan9Mgr: vmPlan9Mgr, noWritableFileShares: noWritableFileShares, - shares: make(map[string]struct{}), + shares: make(map[string]*shareEntry), } } +// ResolveShareName pre-emptively allocates a share name for the given [AddOptions] and returns it. +// If a matching share is already tracked, the existing name is returned without +// allocating a new entry. ResolveShareName does not drive any HCS call or increment the +// reference count; callers must follow up with [Manager.AddToVM] to claim the share. +func (m *Manager) ResolveShareName(ctx context.Context, opts *AddOptions) (string, error) { + if !opts.ReadOnly && m.noWritableFileShares { + return "", fmt.Errorf("adding writable shares is denied: %w", hcs.ErrOperationDenied) + } + + log.G(ctx).WithField(logfields.HostPath, opts.HostPath).Debug("resolving plan9 share name") + + entry := m.getOrAllocateEntry(ctx, opts) + return entry.name, nil +} + // AddToVM adds a Plan9 share to the host VM and returns the generated share name. +// If a share with identical [AddOptions] is already added or in flight, AddToVM +// blocks until that operation completes and returns the share name, incrementing +// the internal reference count. func (m *Manager) AddToVM(ctx context.Context, opts *AddOptions) (_ string, err error) { - m.mu.Lock() - defer m.mu.Unlock() - - // Ensure that adding the share is allowed. + // Validate write-share policy before touching shared state. if !opts.ReadOnly && m.noWritableFileShares { return "", fmt.Errorf("adding writable shares is denied: %w", hcs.ErrOperationDenied) } - // Build the Plan9 share flags bitmask from the caller-provided options. - flags := shareFlagsLinuxMetadata - if opts.ReadOnly { - flags |= shareFlagsReadOnly - } - if opts.Restrict { - flags |= shareFlagsRestrictFileAccess + entry := m.getOrAllocateEntry(ctx, opts) + + // Acquire the per-entry lock to check state and potentially drive the HCS call. + // Multiple goroutines requesting the same share will serialize here. + entry.mu.Lock() + defer entry.mu.Unlock() + + ctx, _ = log.WithContext(ctx, logrus.WithField("shareName", entry.name)) + + log.G(ctx).Debug("received share entry, checking state") + + switch entry.state { + case shareAdded: + // ============================================================================== + // Found an existing live share — reuse it. + // ============================================================================== + entry.refCount++ + log.G(ctx).Debug("plan9 share already added to VM, reusing existing share") + return entry.name, nil + + case sharePending: + // ============================================================================== + // New share — we own the HCS call. + // Other callers requesting the same share will block on entry.mu until we + // transition the state out of sharePending. + // ============================================================================== + flags := shareFlagsLinuxMetadata + if opts.ReadOnly { + flags |= shareFlagsReadOnly + } + if opts.Restrict { + flags |= shareFlagsRestrictFileAccess + } + + log.G(ctx).WithFields(logrus.Fields{ + logfields.HostPath: opts.HostPath, + logfields.ReadOnly: opts.ReadOnly, + "RestrictFileAccess": opts.Restrict, + "AllowedFiles": opts.AllowedNames, + }).Trace("adding plan9 share to host VM") + + if err = m.vmPlan9Mgr.AddPlan9(ctx, hcsschema.Plan9Share{ + Name: entry.name, + AccessName: entry.name, + Path: opts.HostPath, + Port: vmutils.Plan9Port, + Flags: flags, + AllowedFiles: opts.AllowedNames, + }); err != nil { + // Transition to Invalid so that waiting goroutines see the real failure reason. + entry.state = shareInvalid + entry.stateErr = err + + // Remove from the map so subsequent calls can retry with a fresh entry. + m.mu.Lock() + delete(m.shares, entry.name) + m.mu.Unlock() + + return "", fmt.Errorf("add plan9 share %s to host: %w", entry.name, err) + } + + entry.state = shareAdded + entry.refCount++ + + log.G(ctx).Info("plan9 share added to host VM") + + return entry.name, nil + + case shareInvalid: + // ============================================================================== + // A previous AddPlan9 call for this entry failed. + // ============================================================================== + // Return the original error. The map entry has already been removed + // by the goroutine that drove the failed add. + return "", fmt.Errorf("previous attempt to add plan9 share %s to VM failed: %w", + entry.name, entry.stateErr) + + default: + return "", fmt.Errorf("plan9 share in unexpected state %s during add", entry.state) } +} - // Generate a unique share name from the nameCounter. - name := strconv.FormatUint(m.nameCounter, 10) - m.nameCounter++ +// getOrAllocateEntry either reuses an existing [shareEntry] whose options match opts, +// or allocates a new pending entry with a freshly generated name. +// The returned entry's refCount is not incremented; callers that claim the share +// must increment it themselves. +func (m *Manager) getOrAllocateEntry(ctx context.Context, opts *AddOptions) *shareEntry { + m.mu.Lock() + defer m.mu.Unlock() - ctx, _ = log.WithContext(ctx, logrus.WithField("shareName", name)) - - log.G(ctx).WithFields(logrus.Fields{ - logfields.HostPath: opts.HostPath, - logfields.ReadOnly: opts.ReadOnly, - "RestrictFileAccess": opts.Restrict, - "AllowedFiles": opts.AllowedNames, - }).Tracef("adding plan9 share to host VM") - - // Call into HCS to add the Plan9 share to the VM. - if err := m.vmPlan9Mgr.AddPlan9(ctx, hcsschema.Plan9Share{ - Name: name, - AccessName: name, - Path: opts.HostPath, - Port: vmutils.Plan9Port, - Flags: flags, - AllowedFiles: opts.AllowedNames, - }); err != nil { - return "", fmt.Errorf("add plan9 share %s to host: %w", name, err) + // Reuse an existing entry if its options match the caller's. + for _, existing := range m.shares { + if optionsMatch(existing.opts, opts) { + return existing + } } - m.shares[name] = struct{}{} + log.G(ctx).Debug("no existing plan9 share found for options, allocating new entry") - log.G(ctx).Info("plan9 share added to host VM") + name := strconv.FormatUint(m.nameCounter, 10) + m.nameCounter++ - return name, nil + entry := &shareEntry{ + opts: opts, + name: name, + state: sharePending, + // refCount is 0; it will be incremented by the goroutine that drives AddPlan9. + refCount: 0, + } + m.shares[name] = entry + return entry } // RemoveFromVM removes the Plan9 share identified by shareName from the host VM. +// If the share is held by multiple callers, RemoveFromVM decrements the reference +// count and returns without tearing down the share until the last caller removes it. func (m *Manager) RemoveFromVM(ctx context.Context, shareName string) error { - m.mu.Lock() - defer m.mu.Unlock() - ctx, _ = log.WithContext(ctx, logrus.WithField("shareName", shareName)) - if _, ok := m.shares[shareName]; !ok { + m.mu.Lock() + entry := m.shares[shareName] + m.mu.Unlock() + + if entry == nil { log.G(ctx).Debug("plan9 share not found, skipping removal") return nil } - log.G(ctx).Debug("starting plan9 share removal") + entry.mu.Lock() + defer entry.mu.Unlock() - // Call into HCS to remove the share from the VM. - if err := m.vmPlan9Mgr.RemovePlan9(ctx, hcsschema.Plan9Share{ - Name: shareName, - AccessName: shareName, - Port: vmutils.Plan9Port, - }); err != nil { - return fmt.Errorf("remove plan9 share %s from host: %w", shareName, err) + if entry.state == shareInvalid { + // AddPlan9 never succeeded; nothing to remove from HCS. + return nil } - delete(m.shares, shareName) + if entry.refCount > 1 { + entry.refCount-- + log.G(ctx).Debug("plan9 share still in use by other callers, not removing from VM") + return nil + } - log.G(ctx).Info("plan9 share removed from host VM") + // refCount is 0 (pre-allocated via ResolveShareName but never added) or 1 (last caller). + // Only call RemovePlan9 when the share was actually added to the VM. + if entry.state == shareAdded { + log.G(ctx).Debug("starting plan9 share removal") + + if err := m.vmPlan9Mgr.RemovePlan9(ctx, hcsschema.Plan9Share{ + Name: shareName, + AccessName: shareName, + Port: vmutils.Plan9Port, + }); err != nil { + return fmt.Errorf("remove plan9 share %s from host: %w", shareName, err) + } + + entry.state = shareRemoved + log.G(ctx).Info("plan9 share removed from host VM") + } + + // Clean up from the map regardless of whether AddPlan9 was ever called. + m.mu.Lock() + delete(m.shares, shareName) + m.mu.Unlock() return nil } diff --git a/internal/controller/device/plan9/state.go b/internal/controller/device/plan9/state.go new file mode 100644 index 0000000000..edd8212bf8 --- /dev/null +++ b/internal/controller/device/plan9/state.go @@ -0,0 +1,55 @@ +//go:build windows && !wcow + +package plan9 + +// shareState represents the current state of a Plan9 share's lifecycle. +// +// The normal progression is: +// +// sharePending → shareAdded → shareRemoved +// +// If AddPlan9 fails, the owning goroutine moves the share to +// shareInvalid and records the error in [shareEntry.stateErr]. Other goroutines +// waiting on the same entry observe the invalid state and receive the original error. +// The entry is removed from the map immediately after the transition. +// +// Full state-transition table: +// +// Current State │ Trigger │ Next State +// ───────────────┼───────────────────────────┼───────────────────────────── +// sharePending │ AddPlan9 succeeds │ shareAdded +// sharePending │ AddPlan9 fails │ shareInvalid +// shareAdded │ RemovePlan9 succeeds │ shareRemoved +// shareRemoved │ (terminal — no transitions)│ — +// shareInvalid │ entry removed from map │ — +type shareState int + +const ( + // sharePending is the initial state; AddPlan9 has not yet completed. + sharePending shareState = iota + + // shareAdded means AddPlan9 succeeded; the share is live on the VM. + shareAdded + + // shareRemoved means RemovePlan9 succeeded. This is a terminal state. + shareRemoved + + // shareInvalid means AddPlan9 failed. + shareInvalid +) + +// String returns a human-readable name for the [shareState]. +func (s shareState) String() string { + switch s { + case sharePending: + return "Pending" + case shareAdded: + return "Added" + case shareRemoved: + return "Removed" + case shareInvalid: + return "Invalid" + default: + return "Unknown" + } +} diff --git a/internal/controller/device/plan9/types.go b/internal/controller/device/plan9/types.go new file mode 100644 index 0000000000..5828774bc0 --- /dev/null +++ b/internal/controller/device/plan9/types.go @@ -0,0 +1,85 @@ +//go:build windows && !wcow + +package plan9 + +import ( + "context" + "sync" + + hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" +) + +// AddOptions holds the configuration required to add a Plan9 share to the VM. +type AddOptions struct { + // HostPath is the path on the host to share into the VM. + HostPath string + + // ReadOnly indicates whether the share should be mounted read-only. + ReadOnly bool + + // Restrict enables single-file mapping mode for the share. + Restrict bool + + // AllowedNames is the list of file names allowed when Restrict is true. + AllowedNames []string +} + +// vmPlan9Manager manages adding and removing Plan9 shares on the host VM. +// Implemented by [vmmanager.UtilityVM]. +type vmPlan9Manager interface { + // AddPlan9 adds a plan 9 share to a running Utility VM. + AddPlan9(ctx context.Context, settings hcsschema.Plan9Share) error + + // RemovePlan9 removes a plan 9 share from a running Utility VM. + RemovePlan9(ctx context.Context, settings hcsschema.Plan9Share) error +} + +// ============================================================================== +// INTERNAL DATA STRUCTURES +// Types below this line are unexported and used for state tracking. +// ============================================================================== + +// shareEntry records one Plan9 share's full lifecycle state and reference count. +type shareEntry struct { + // mu serializes state transitions. + mu sync.Mutex + + // opts is the immutable share parameters used to match duplicate add requests. + opts *AddOptions + + // name is the HCS-level identifier for this share, generated at allocation time. + name string + + // refCount is the number of active callers sharing this entry. + // Access must be guarded by [Manager.mu]. + refCount uint + + // state tracks the forward-only lifecycle position of this share. + // Access must be guarded by mu. + state shareState + + // stateErr records the error that caused a transition to [shareInvalid]. + // Waiters that find the entry in the invalid state return this error so + // that every caller sees the original failure reason. + stateErr error +} + +// optionsMatch reports whether two [AddOptions] values describe the same share. +// AllowedNames is compared in order. +func optionsMatch(a, b *AddOptions) bool { + if a == nil || b == nil { + return a == b + } + if a.HostPath != b.HostPath || a.ReadOnly != b.ReadOnly || a.Restrict != b.Restrict { + return false + } + if len(a.AllowedNames) != len(b.AllowedNames) { + return false + } + for i := range a.AllowedNames { + if a.AllowedNames[i] != b.AllowedNames[i] { + return false + } + } + return true +}