diff --git a/docs/implementation-plan.v1.md b/docs/implementation-plan.v1.md index 5be5bc0..cbbf4a4 100644 --- a/docs/implementation-plan.v1.md +++ b/docs/implementation-plan.v1.md @@ -1157,8 +1157,10 @@ Use one declared test matrix and the same harness entry points everywhere: native Windows integration suites, plus `smoke`, `idempotency`, `reconnect`, and `retention` E2E scenarios; - release candidates run every E2E scenario, including the resettable - interactive Windows VM matrix, Server Core smoke, destructive fault cases, - upgrade/recovery, and soak. + interactive Windows 10 VM matrix, destructive fault cases, upgrade/recovery, + and soak. Server Core, older-build, and ambiguous-multi-session smoke lanes + run when their dedicated fixtures are provisioned; they are explicitly + deferred compatibility work and do not block the Windows 10 v1 baseline. CI allocates a run ID per job, always invokes `collect` after failure, and invokes `reset` in an unconditional finalizer. Upload only the bounded redacted report, diff --git a/docs/testing.md b/docs/testing.md index 6c90a60..ad44ef4 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -286,6 +286,8 @@ execution context. The normal `rvboxtest` console session remains the subject of active-user and elevation tests. See [testing-vm.md](testing-vm.md) for the exact fixture contract. -The VM is the minimum smoke lane, so native multi-session/ambiguous-session, -Server Core, and older-build entries remain explicitly blocked until their own -fixtures exist. Their pure selector tests remain mandatory. +The VM is the current Windows 10 v1 smoke lane. Native +multi-session/ambiguous-session, Server Core, and older-build entries are +explicitly deferred compatibility work until their own fixtures exist; they do +not block the Windows 10 v1 baseline. Their pure selector tests remain +mandatory. diff --git a/internal/domain/cursor_test.go b/internal/domain/cursor_test.go index ccea608..1d33f80 100644 --- a/internal/domain/cursor_test.go +++ b/internal/domain/cursor_test.go @@ -72,3 +72,24 @@ func TestCursorInputBounds_BH_CTL_01(t *testing.T) { } } } + +// FuzzDecodeCursorBounded_SEC_CTL_02 exercises the token boundary shared by +// every cursor-resumable control read. Decode must reject malformed or forged +// input without panicking or allocating past its documented token ceiling. +func FuzzDecodeCursorBounded_SEC_CTL_02(f *testing.F) { + codec, err := NewCursorCodec(bytes.Repeat([]byte{0x42}, 32)) + if err != nil { + f.Fatal(err) + } + filters := HashCursorFilters([]byte("client=host-1\x00streams=stdout")) + valid, err := codec.Encode(Cursor{Kind: CursorKindOutput, FilterHash: filters, Position: []byte("event/offset"), SnapshotBoundary: []byte("upper-event")}) + if err != nil { + f.Fatal(err) + } + for _, seed := range []string{"", "***", valid, strings.Repeat("a", 4096)} { + f.Add(seed) + } + f.Fuzz(func(t *testing.T, token string) { + _, _ = codec.Decode(token, CursorKindOutput, filters) + }) +} diff --git a/internal/server/control/service.go b/internal/server/control/service.go index fb22cf3..8280041 100644 --- a/internal/server/control/service.go +++ b/internal/server/control/service.go @@ -256,6 +256,7 @@ func (service *Service) RunCommand(ctx context.Context, request *rvboxv1.RunComm descriptor := spec.GetScript() digest := sha256.Sum256(request.GetScriptContent()) if descriptor == nil || uint64(len(request.GetScriptContent())) != descriptor.GetSizeBytes() || !bytes.Equal(digest[:], descriptor.GetSha256()) { + service.incMetric("script_verification_failure") return nil, controlError(codes.InvalidArgument, rvboxv1.ControlError_INVALID_ARGUMENT, "script_content does not match the script descriptor") } } diff --git a/internal/server/control/service_test.go b/internal/server/control/service_test.go index ea90d31..4ba006e 100644 --- a/internal/server/control/service_test.go +++ b/internal/server/control/service_test.go @@ -11,6 +11,7 @@ import ( rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1" "github.com/rvbox/rvbox/internal/agentproto" "github.com/rvbox/rvbox/internal/domain" + "github.com/rvbox/rvbox/internal/observability" "github.com/rvbox/rvbox/internal/server/store" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -70,6 +71,8 @@ func TestControlListAndGetViews_HP_CONTROL_01(t *testing.T) { func TestRunCommandRequestIDIdempotencyAndValidation_BH_CONTROL_02(t *testing.T) { service, persistence := newTestService(t) defer persistence.Close() + metrics := observability.New() + service.metrics = metrics ctx := context.Background() registerControlClient(t, persistence, "win-a", rvboxv1.Platform_PLATFORM_WINDOWS, rvboxv1.ShellType_SHELL_POWERSHELL, 3) issue := fixedIssue(0xa2) @@ -102,6 +105,10 @@ func TestRunCommandRequestIDIdempotencyAndValidation_BH_CONTROL_02(t *testing.T) if _, err := service.RunCommand(ctx, badScript); status.Code(err) != codes.InvalidArgument { t.Fatalf("mismatched script code = %v", status.Code(err)) } + _, _, counters := metrics.Snapshot() + if counters["script_verification_failure"] != 1 { + t.Fatalf("script verification metrics = %#v", counters) + } badTTL := proto.Clone(request).(*rvboxv1.RunCommandRequest) badTTL.RequestId = fixedIssue(0xa4).String() badTTL.QueueTtl = durationpb.New(-time.Second) diff --git a/test/coverage.toml b/test/coverage.toml index 94c1ecc..546b0f1 100644 --- a/test/coverage.toml +++ b/test/coverage.toml @@ -182,6 +182,7 @@ tests = [ "internal/agentproto/validate_test.go:FuzzDecodeOutputChunkBounded_SEC_PROTO_02", "internal/server/control/jsonrpc_test.go:FuzzDecodeJSONRPCRequestBounded_SEC_CTL_01", "internal/server/store/segment_test.go:FuzzDecodeSegmentRecordDoesNotEscapeBounds", + "internal/domain/cursor_test.go:FuzzDecodeCursorBounded_SEC_CTL_02", ] [[requirements]]