From def087ed90b022043f4edde4a964516e940447c8 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:41:40 +0100 Subject: [PATCH] Reproduce unverified VM command admission at the host agent Approvals and trusted Discovery commands currently cross QGA handoff without evidence of a local unlocked guest. Assert explicit no-handoff deferral without contacting a native target. Change-source: pulse-maintainer --- internal/hostagent/command_client_test.go | 4 ++ .../commands_guest_safety_regression_test.go | 43 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 internal/hostagent/commands_guest_safety_regression_test.go diff --git a/internal/hostagent/command_client_test.go b/internal/hostagent/command_client_test.go index 5014487456..22c2d13d08 100644 --- a/internal/hostagent/command_client_test.go +++ b/internal/hostagent/command_client_test.go @@ -514,3 +514,7 @@ func TestCommandClient_ReplayedRequestWaitsForInFlightHandlerInsteadOfDropping(t t.Fatal("replay handler did not release the slot") } } + +func TestCommandClientGuestExecutionAdmissionContract(t *testing.T) { + testVMGuestExecNoUnverifiedHandoff(t) +} diff --git a/internal/hostagent/commands_guest_safety_regression_test.go b/internal/hostagent/commands_guest_safety_regression_test.go new file mode 100644 index 0000000000..29c7e71e1a --- /dev/null +++ b/internal/hostagent/commands_guest_safety_regression_test.go @@ -0,0 +1,43 @@ +package hostagent + +import ( + "context" + "os/exec" + "strings" + "sync/atomic" + "testing" +) + +// No PVE config exists in the offline guest. A command policy approval (or +// Pulse's internal Trusted marker) is not evidence that this VM is unlocked. +func testVMGuestExecNoUnverifiedHandoff(t *testing.T) { + for _, target := range []struct{ kind, id string }{ + {"vm", "105"}, {" VM ", "105"}, {"vm", "000105"}, + {"vm", "0"}, {"vm", "not-a-vmid"}, + } { + for _, trusted := range []bool{false, true} { + t.Run(target.kind+"/"+target.id+"/trusted="+map[bool]string{true: "yes", false: "no"}[trusted], func(t *testing.T) { + var handoffs atomic.Int32 + oldExec := execCommandContext + execCommandContext = func(ctx context.Context, name string, args ...string) *exec.Cmd { + handoffs.Add(1) + // Simulate a successful provider boundary, never invoke QGA. + return exec.CommandContext(ctx, "sh", "-c", "printf 'guest-output'") + } + t.Cleanup(func() { execCommandContext = oldExec }) + c := &CommandClient{} + payload := testApprovedCommandPayload(t, c, executeCommandPayload{ + RequestID: "unverified-vm", Command: "echo inspected", TargetType: target.kind, + TargetID: target.id, Trusted: trusted, + }) + got := c.executeCommand(context.Background(), payload) + if handoffs.Load() != 0 { + t.Errorf("unverified VM crossed command handoff %d times", handoffs.Load()) + } + if got.Success || got.Stdout != "" || !strings.Contains(got.Error, "guest execution deferred") { + t.Errorf("unverified VM was not an explicit output-free deferral: %#v", got) + } + }) + } + } +}