feat: add native Windows hierarchy test harness
This commit is contained in:
@@ -37,7 +37,10 @@ func syncDirectory(path string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handle, err := windows.CreateFile(value, windows.GENERIC_READ, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, 0)
|
||||
// FlushFileBuffers requires GENERIC_WRITE even when the handle refers to a
|
||||
// directory. Opening it read-only succeeds, then deterministically fails
|
||||
// the durability barrier with ERROR_ACCESS_DENIED on Windows.
|
||||
handle, err := windows.CreateFile(value, windows.GENERIC_READ|windows.GENERIC_WRITE, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -18,11 +18,9 @@ const privateStateSDDL = "D:P(A;;FA;;;SY)(A;;FA;;;BA)"
|
||||
func ensurePrivateFile(path string) error {
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o600)
|
||||
if err == nil {
|
||||
err = file.Close()
|
||||
} else if !errors.Is(err, os.ErrExist) {
|
||||
return err
|
||||
return file.Close()
|
||||
}
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrExist) {
|
||||
return err
|
||||
}
|
||||
info, err := os.Lstat(path)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
//go:build windows
|
||||
|
||||
package spool
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnsurePrivateFileAcceptsExistingPrivateFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "spool.lock")
|
||||
if err := ensurePrivateFile(path); err != nil {
|
||||
t.Fatalf("create private file: %v", err)
|
||||
}
|
||||
if err := ensurePrivateFile(path); err != nil {
|
||||
t.Fatalf("recheck existing private file: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -116,8 +117,7 @@ func Open(ctx context.Context, options Options) (*Store, error) {
|
||||
query.Add("_pragma", "foreign_keys(ON)")
|
||||
query.Add("_pragma", "synchronous(FULL)")
|
||||
query.Add("_pragma", "busy_timeout("+strconv.FormatInt(options.BusyTimeout.Milliseconds(), 10)+")")
|
||||
databaseURL := &url.URL{Scheme: "file", Path: filepath.ToSlash(databasePath), RawQuery: query.Encode()}
|
||||
db, err := sql.Open("sqlite", databaseURL.String())
|
||||
db, err := sql.Open("sqlite", sqliteFileURI(databasePath, query))
|
||||
if err != nil {
|
||||
_ = unlock()
|
||||
return nil, err
|
||||
@@ -136,6 +136,17 @@ func Open(ctx context.Context, options Options) (*Store, error) {
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// sqliteFileURI creates a file: URI with no authority. SQLite requires a
|
||||
// Windows drive path to be /C:/... in the URI path; without the leading slash
|
||||
// net/url renders C: as an authority (file://C:/...), which SQLite rejects.
|
||||
func sqliteFileURI(path string, query url.Values) string {
|
||||
path = strings.ReplaceAll(path, `\`, "/")
|
||||
if len(path) >= 2 && path[1] == ':' {
|
||||
path = "/" + path
|
||||
}
|
||||
return (&url.URL{Scheme: "file", Path: path, RawQuery: query.Encode()}).String()
|
||||
}
|
||||
|
||||
func (store *Store) ClientInstanceID() domain.UUID { return store.identity }
|
||||
|
||||
func (store *Store) Close() error {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -16,6 +17,13 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestSQLiteFileURIWindowsDrivePath(t *testing.T) {
|
||||
query := url.Values{"_pragma": {"journal_mode(WAL)"}}
|
||||
if got, want := sqliteFileURI(`C:\ProgramData\RVBox\test-state\spool.db`, query), "file:///C:/ProgramData/RVBox/test-state/spool.db?_pragma=journal_mode%28WAL%29"; got != want {
|
||||
t.Fatalf("sqliteFileURI() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecutionSpecIsDurableAndQuotaCounted_HP_DISPATCH_07(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -43,7 +43,13 @@ type NativeOptions struct {
|
||||
MaxWrapperBytes uint64
|
||||
MaxOutputChunk uint64
|
||||
WindowsTermGrace time.Duration
|
||||
Now func() time.Time
|
||||
// TestContextFailures is populated only by the separately tagged native
|
||||
// fixture binary. It is deliberately not protocol or TOML policy: it lets
|
||||
// the fixture fail token preparation before launch so the real daemon can
|
||||
// prove its fallback order without creating a second broker or weakening a
|
||||
// release binary.
|
||||
TestContextFailures map[ExecutionContext]bool
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
// JobProfile is the validated administrator policy for one protocol profile.
|
||||
|
||||
@@ -388,6 +388,10 @@ func (manager *execSupervisor) selectToken(elevated bool) (winapi.Token, supervi
|
||||
}
|
||||
for _, attempt := range selection.Attempts {
|
||||
token, identity, err := openTokenForAttempt(attempt.Context, selected)
|
||||
if err == nil && manager.options.TestContextFailures[attempt.Context] {
|
||||
_ = token.Close()
|
||||
err = errors.New("native test fixture forced pre-launch context failure")
|
||||
}
|
||||
if err == nil {
|
||||
return token, withEvidence(identity), nil
|
||||
}
|
||||
@@ -396,6 +400,33 @@ func (manager *execSupervisor) selectToken(elevated bool) (winapi.Token, supervi
|
||||
return 0, supervisor.EffectiveIdentity{}, rejection(err)
|
||||
}
|
||||
}
|
||||
// Select stops at the first policy-available elevated context. Native
|
||||
// preparation can still fail after that point (a linked token can vanish or
|
||||
// SeTcbPrivilege can be unavailable), so preserve the documented order by
|
||||
// trying ACTIVE_SYSTEM before the final LOCAL_SYSTEM fallback. This is a
|
||||
// preparation fallback only: no child has been created yet.
|
||||
if elevated && selected != nil {
|
||||
seenActiveSystem := false
|
||||
for _, contextName := range attempted {
|
||||
if contextName == string(ContextActiveSystem) {
|
||||
seenActiveSystem = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seenActiveSystem {
|
||||
addAttempt(ContextActiveSystem, "fallback after active-user-elevated preparation failure")
|
||||
if token, identity, err := openTokenForAttempt(ContextActiveSystem, selected); err == nil {
|
||||
if manager.options.TestContextFailures[ContextActiveSystem] {
|
||||
_ = token.Close()
|
||||
addAttempt(ContextActiveSystem, "native test fixture forced pre-launch context failure")
|
||||
} else {
|
||||
return token, withEvidence(identity), nil
|
||||
}
|
||||
} else {
|
||||
addAttempt(ContextActiveSystem, "native preparation failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
// The pure selector stops as soon as ACTIVE_SYSTEM is available. A native
|
||||
// privilege/session operation can still fail (for example, SeTcb was
|
||||
// removed), so the final LOCAL_SYSTEM fallback is attempted here before
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build !rvbox_native_test
|
||||
|
||||
package windows
|
||||
|
||||
import "fmt"
|
||||
|
||||
// NativeTestContextFailures is intentionally unavailable in shipped binaries.
|
||||
// The native fixture compiles a separately tagged test executable when it
|
||||
// needs to prove a pre-launch fallback row.
|
||||
func NativeTestContextFailures(raw string) (map[ExecutionContext]bool, error) {
|
||||
if raw != "" {
|
||||
return nil, fmt.Errorf("--test-fail-contexts requires the rvbox_native_test fixture build")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//go:build rvbox_native_test
|
||||
|
||||
package windows
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NativeTestContextFailures accepts only the two active elevated preparation
|
||||
// stages. It is compiled into the disposable native-fixture binary, never a
|
||||
// release binary, and is applied before a process is launched.
|
||||
func NativeTestContextFailures(raw string) (map[ExecutionContext]bool, error) {
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
result := make(map[ExecutionContext]bool)
|
||||
for _, item := range strings.Split(raw, ",") {
|
||||
contextName := ExecutionContext(strings.TrimSpace(item))
|
||||
switch contextName {
|
||||
case ContextActiveUserElevated, ContextActiveSystem:
|
||||
result[contextName] = true
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported native test context %q", item)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -74,8 +74,13 @@ func newTrayPipe() (*os.File, error) {
|
||||
Length: uint32(unsafe.Sizeof(winapi.SecurityAttributes{})),
|
||||
SecurityDescriptor: descriptor,
|
||||
}
|
||||
mode := uint32(winapi.PIPE_ACCESS_DUPLEX | winapi.PIPE_TYPE_MESSAGE | winapi.PIPE_READMODE_MESSAGE | winapi.PIPE_WAIT | winapi.PIPE_REJECT_REMOTE_CLIENTS | winapi.SECURITY_IDENTIFICATION)
|
||||
handle, err := winapi.CreateNamedPipe(name, mode, pipeInstances, pipeBufferBytes, pipeBufferBytes, 0, 0, attributes)
|
||||
// CreateNamedPipe has separate open-mode and pipe-mode arguments. Remote
|
||||
// client rejection belongs to the latter; placing it in open mode produces
|
||||
// ERROR_INVALID_PARAMETER on Windows. We inspect the client token directly
|
||||
// and never impersonate it, so no SQOS/impersonation flag is needed here.
|
||||
openMode := uint32(winapi.PIPE_ACCESS_DUPLEX)
|
||||
pipeMode := uint32(winapi.PIPE_TYPE_MESSAGE | winapi.PIPE_READMODE_MESSAGE | winapi.PIPE_WAIT | winapi.PIPE_REJECT_REMOTE_CLIENTS)
|
||||
handle, err := winapi.CreateNamedPipe(name, openMode, pipeMode, pipeInstances, pipeBufferBytes, pipeBufferBytes, 0, attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user