Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Go bindflt/silo definitions #1331

Merged
merged 2 commits into from
Apr 4, 2022
Merged
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 internal/exec/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func TestExecsWithJob(t *testing.T) {
}

if len(pids) != 2 {
t.Fatalf("should be two pids in job object, got: %d", len(pids))
t.Fatalf("should be two pids in job object, got: %d. Pids: %+v", len(pids), pids)
}

for _, pid := range pids {
Expand Down
118 changes: 117 additions & 1 deletion internal/jobobject/jobobject.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package jobobject
import (
"context"
"fmt"
"os"
"path/filepath"
"sync"
"sync/atomic"
"unsafe"

"github.com/Microsoft/hcsshim/internal/queue"
Expand All @@ -24,7 +27,10 @@ import (
// the job, a queue to receive iocp notifications about the lifecycle
// of the job and a mutex for synchronized handle access.
type JobObject struct {
handle windows.Handle
handle windows.Handle
// All accesses to this MUST be done atomically. 1 signifies that this
// job is currently a silo.
isAppSilo uint32
mq *queue.MessageQueue
handleLock sync.RWMutex
}
Expand Down Expand Up @@ -56,6 +62,7 @@ const (
var (
ErrAlreadyClosed = errors.New("the handle has already been closed")
ErrNotRegistered = errors.New("job is not registered to receive notifications")
ErrNotSilo = errors.New("job is not a silo")
)

// Options represents the set of configurable options when making or opening a job object.
Expand All @@ -68,6 +75,9 @@ type Options struct {
// `UseNTVariant` specifies if we should use the `Nt` variant of Open/CreateJobObject.
// Defaults to false.
UseNTVariant bool
// `Silo` specifies to promote the job to a silo. This additionally sets the flag
// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE as it is required for the upgrade to complete.
Silo bool
}

// Create creates a job object.
Expand Down Expand Up @@ -134,6 +144,16 @@ func Create(ctx context.Context, options *Options) (_ *JobObject, err error) {
job.mq = mq
}

if options.Silo {
// This is a required setting for upgrading to a silo.
if err := job.SetTerminateOnLastHandleClose(); err != nil {
return nil, err
}
if err := job.PromoteToSilo(); err != nil {
return nil, err
}
}

return job, nil
}

Expand Down Expand Up @@ -436,3 +456,99 @@ func (job *JobObject) QueryStorageStats() (*winapi.JOBOBJECT_BASIC_AND_IO_ACCOUN
}
return &info, nil
}

// ApplyFileBinding makes a file binding using the Bind Filter from target to root. If the job has
// not been upgraded to a silo this call will fail. The binding is only applied and visible for processes
// running in the job, any processes on the host or in another job will not be able to see the binding.
func (job *JobObject) ApplyFileBinding(root, target string, merged bool) error {
job.handleLock.RLock()
dcantah marked this conversation as resolved.
Show resolved Hide resolved
defer job.handleLock.RUnlock()

if job.handle == 0 {
return ErrAlreadyClosed
}

if !job.isSilo() {
return ErrNotSilo
}

// The parent directory needs to exist for the bind to work.
if _, err := os.Stat(filepath.Dir(root)); err != nil {
if !os.IsNotExist(err) {
return err
}
if err := os.MkdirAll(filepath.Dir(root), 0); err != nil {
return err
}
}

rootPtr, err := windows.UTF16PtrFromString(root)
if err != nil {
return err
}

targetPtr, err := windows.UTF16PtrFromString(target)
if err != nil {
return err
}

flags := winapi.BINDFLT_FLAG_USE_CURRENT_SILO_MAPPING
if merged {
flags |= winapi.BINDFLT_FLAG_MERGED_BIND_MAPPING
}

if err := winapi.BfSetupFilterEx(
flags,
job.handle,
nil,
rootPtr,
targetPtr,
nil,
0,
); err != nil {
return fmt.Errorf("failed to bind target %q to root %q for job object: %w", target, root, err)
}
return nil
}

// PromoteToSilo promotes a job object to a silo. There must be no running processess
// in the job for this to succeed. If the job is already a silo this is a no-op.
func (job *JobObject) PromoteToSilo() error {
dcantah marked this conversation as resolved.
Show resolved Hide resolved
job.handleLock.RLock()
defer job.handleLock.RUnlock()

if job.handle == 0 {
return ErrAlreadyClosed
helsaawy marked this conversation as resolved.
Show resolved Hide resolved
}

if job.isSilo() {
return nil
}

pids, err := job.Pids()
if err != nil {
return err
}

if len(pids) != 0 {
return fmt.Errorf("job cannot have running processes to be promoted to a silo, found %d running processes", len(pids))
}

_, err = windows.SetInformationJobObject(
job.handle,
winapi.JobObjectCreateSilo,
0,
0,
)
if err != nil {
return fmt.Errorf("failed to promote job to silo: %w", err)
}

atomic.StoreUint32(&job.isAppSilo, 1)
return nil
}

// isSilo returns if the job object is a silo.
func (job *JobObject) isSilo() bool {
return atomic.LoadUint32(&job.isAppSilo) == 1
helsaawy marked this conversation as resolved.
Show resolved Hide resolved
}
69 changes: 69 additions & 0 deletions internal/jobobject/jobobject_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package jobobject

import (
"context"
"os"
"os/exec"
"path/filepath"
"syscall"
"testing"
"time"
Expand Down Expand Up @@ -281,3 +283,70 @@ func TestVerifyPidCount(t *testing.T) {
t.Fatal(err)
}
}

func TestSilo(t *testing.T) {
// Test asking for a silo in the options.
options := &Options{
Silo: true,
}
job, err := Create(context.Background(), options)
if err != nil {
t.Fatal(err)
}
defer job.Close()
}

func TestSiloFileBinding(t *testing.T) {
// Can't use osversion as the binary needs to be manifested for it to work.
// Just stat for the bindflt dll.
if _, err := os.Stat(`C:\windows\system32\bindfltapi.dll`); err != nil {
t.Skip("Bindflt not present on RS5 or lower, skipping.")
}
// Test upgrading to a silo and binding a file only the silo can see.
options := &Options{
Silo: true,
}
job, err := Create(context.Background(), options)
if err != nil {
t.Fatal(err)
}
defer job.Close()

target := t.TempDir()
hostPath := filepath.Join(target, "bind-test.txt")
f, err := os.Create(hostPath)
if err != nil {
t.Fatal(err)
}
defer f.Close()

root := t.TempDir()
siloPath := filepath.Join(root, "silo-path.txt")
if err := job.ApplyFileBinding(siloPath, hostPath, false); err != nil {
t.Fatal(err)
}

// First check that we can't see the file on the host.
if _, err := os.Stat(siloPath); err == nil {
t.Fatalf("expected to not be able to see %q on the host", siloPath)
}

// Now check that we can see it in the silo. Couple second timeout (ping something) so
// we can be relatively sure the process has been assigned to the job before we go to check
// on the file. Unfortunately we can't use our internal/exec package that has support for
// assigning a process to a job at creation time as it causes a cyclical import.
cmd := exec.Command("cmd", "/c", "ping", "localhost", "&&", "dir", siloPath)
if err := cmd.Start(); err != nil {
t.Fatal(err)
}

if err := job.Assign(uint32(cmd.Process.Pid)); err != nil {
t.Fatal(err)
}

// Process will have an exit code of 1 if dir couldn't find the file; if we get
// no error here we should be A-OK.
if err := cmd.Wait(); err != nil {
t.Fatal(err)
}
}
20 changes: 20 additions & 0 deletions internal/winapi/bindflt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package winapi

const (
BINDFLT_FLAG_READ_ONLY_MAPPING uint32 = 0x00000001
BINDFLT_FLAG_MERGED_BIND_MAPPING uint32 = 0x00000002
BINDFLT_FLAG_USE_CURRENT_SILO_MAPPING uint32 = 0x00000004
)

// HRESULT
// BfSetupFilterEx(
// _In_ ULONG Flags,
// _In_opt_ HANDLE JobHandle,
// _In_opt_ PSID Sid,
// _In_ LPCWSTR VirtualizationRootPath,
// _In_ LPCWSTR VirtualizationTargetPath,
// _In_reads_opt_( VirtualizationExceptionPathCount ) LPCWSTR* VirtualizationExceptionPaths,
// _In_opt_ ULONG VirtualizationExceptionPathCount
// );
//
//sys BfSetupFilterEx(flags uint32, jobHandle windows.Handle, sid *windows.SID, virtRootPath *uint16, virtTargetPath *uint16, virtExceptions **uint16, virtExceptionPathCount uint32) (hr error) = bindfltapi.BfSetupFilterEx?
1 change: 1 addition & 0 deletions internal/winapi/jobobject.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const (
JobObjectLimitViolationInformation uint32 = 13
JobObjectMemoryUsageInformation uint32 = 28
JobObjectNotificationLimitInformation2 uint32 = 33
JobObjectCreateSilo uint32 = 35
JobObjectIoAttribution uint32 = 42
)

Expand Down
2 changes: 1 addition & 1 deletion internal/winapi/winapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
// be thought of as an extension to golang.org/x/sys/windows.
package winapi

//go:generate go run ..\..\mksyscall_windows.go -output zsyscall_windows.go user.go console.go system.go net.go path.go thread.go jobobject.go logon.go memory.go process.go processor.go devices.go filesystem.go errors.go
//go:generate go run ..\..\mksyscall_windows.go -output zsyscall_windows.go bindflt.go user.go console.go system.go net.go path.go thread.go jobobject.go logon.go memory.go process.go processor.go devices.go filesystem.go errors.go
30 changes: 23 additions & 7 deletions internal/winapi/zsyscall_windows.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading