feat: add native Windows hierarchy test harness

This commit is contained in:
2026-09-09 16:48:54 +00:00
parent 8985457d37
commit 409b64a2fb
30 changed files with 1033 additions and 44 deletions
+4 -1
View File
@@ -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
}
+2 -4
View File
@@ -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)
}
}
+13 -2
View File
@@ -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 {
+8
View File
@@ -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
}
+11
View File
@@ -49,6 +49,17 @@ func TestAnnotatedExamples_HP_CFG_01(t *testing.T) {
}
}
func TestClientDefaultsDecodeForWindowsWithoutNativeFilesystem(t *testing.T) {
t.Parallel()
client, err := DecodeClient([]byte("[client]\nstate_dir = \"C:\\\\ProgramData\\\\RVBox\\\\state\"\ndaemon_cwd = \"C:\\\\ProgramData\\\\RVBox\\\\work\"\n"), ClientOptions{Platform: PlatformWindows, CheckFilesystem: false})
if err != nil {
t.Fatalf("DecodeClient Windows defaults: %v", err)
}
if client.Shells.SH != "/bin/sh" || client.Shells.CMD == "" {
t.Fatalf("Windows defaults have unexpected shell paths: %+v", client.Shells)
}
}
func TestStrictTOMLRejections_HP_CFG_01(t *testing.T) {
t.Parallel()
+6 -2
View File
@@ -3,6 +3,7 @@ package config
import (
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"strings"
@@ -21,10 +22,13 @@ func validateAbsolutePath(name, value string, platform Platform, allowEmpty bool
}
return cleanWindowsPath(value), nil
}
if !filepath.IsAbs(value) {
// Config parsing can validate an inactive Unix shell path while running on
// Windows. filepath follows the host OS, whereas the config field's stated
// platform is Unix, so use slash-path semantics here.
if !path.IsAbs(value) {
return "", fmt.Errorf("%s must be an absolute Unix path", name)
}
return filepath.Clean(value), nil
return path.Clean(value), nil
}
func isWindowsAbsolute(value string) bool {