From 6686ca3fdcc9ee25135a288b75fa05f7e6e31cdc Mon Sep 17 00:00:00 2001 From: ErenAri Date: Wed, 17 Jun 2026 02:21:39 +0300 Subject: [PATCH 1/2] Sanitize public validation reports; fix host-dependent arm64 test Two fixes: 1. Strip host-internal fields from validation reports served over HTTP. The finished-run JSON (web validate flow + history run-report endpoint, both anonymous-readable on the public demo) embedded absolute host paths, the per-target VM run directory (which also holds that run's SSH private key), the full QEMU command line, and the serial-log path. New internal/api/sanitize.go clears these before the report leaves the host; the compatibility evidence (load/attach/verifier/BTF/functional results, kernel info, classification, timings) is preserved. The on-disk report is untouched for operators with shell access. 2. Fix arm64-build-smoke failure. TestQEMUSystemBinaryForARM64 pinned "-machine virt,accel=kvm", but the accel mode depends on whether the host exposes /dev/kvm: KVM locally and on x86 CI, TCG on the arm64 runner. The test now asserts only the host-independent arm64 shape ("-machine virt,accel=...", not the x86 -enable-kvm form); explicit kvm/tcg coverage already lives in TestMachineArgsForAccelFallback. Co-Authored-By: Claude Opus 4.8 --- internal/api/handlers_history.go | 2 +- internal/api/sanitize.go | 69 +++++++++++++++++++++++ internal/api/sanitize_test.go | 95 ++++++++++++++++++++++++++++++++ internal/api/server.go | 24 +++----- internal/vm/qemu_test.go | 5 +- 5 files changed, 176 insertions(+), 19 deletions(-) create mode 100644 internal/api/sanitize.go create mode 100644 internal/api/sanitize_test.go diff --git a/internal/api/handlers_history.go b/internal/api/handlers_history.go index e9e4729..f728114 100644 --- a/internal/api/handlers_history.go +++ b/internal/api/handlers_history.go @@ -88,5 +88,5 @@ func (s *Server) handleRunReport(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusInternalServerError, fmt.Sprintf("parse report: %v", err)) return } - writeJSON(w, http.StatusOK, map[string]any{"run_id": runID, "report": report}) + writeJSON(w, http.StatusOK, map[string]any{"run_id": runID, "report": sanitizeReportMapForPublic(report)}) } diff --git a/internal/api/sanitize.go b/internal/api/sanitize.go new file mode 100644 index 0000000..6affbca --- /dev/null +++ b/internal/api/sanitize.go @@ -0,0 +1,69 @@ +package api + +import ( + "github.com/kernel-guard/bpfcompat/internal/runner" + "github.com/kernel-guard/bpfcompat/pkg/schema" +) + +// The web validate/history endpoints are reachable by anonymous visitors on the +// public demo. The full report carries operator-only, host-internal details that +// should never leave the host over HTTP: +// - absolute filesystem paths (report/markdown/artifact paths, run_dir) +// - the per-target VM run directory, which also holds that run's SSH private key +// - the per-target QEMU command line and serial-log path +// +// The compatibility evidence a viewer actually wants (load/attach/verifier/BTF/ +// functional results, kernel info, classification, timings) is preserved. + +// sanitizeReportForPublic returns a copy of the typed report with the +// host-internal fields cleared. The original on-disk report is untouched. +func sanitizeReportForPublic(r schema.ReportV01) schema.ReportV01 { + r.Paths = schema.Paths{} + r.Artifact.Path = "" + r.Matrix.Path = "" + if len(r.Targets) > 0 { + targets := make([]schema.Target, len(r.Targets)) + copy(targets, r.Targets) + for i := range targets { + targets[i].VMRunDir = "" + targets[i].QEMUCommand = "" + targets[i].SerialLog = "" + } + r.Targets = targets + } + return r +} + +// publicValidateResponse builds a validateResponse safe to return over HTTP: +// host paths are dropped and the embedded report is sanitized. +func publicValidateResponse(result runner.RunResult) *validateResponse { + return &validateResponse{ + ExitCode: result.ExitCode, + Report: sanitizeReportForPublic(result.Report), + } +} + +// sanitizeReportMapForPublic strips the same host-internal fields from a report +// decoded from disk as a generic map (the history run-report endpoint). +func sanitizeReportMapForPublic(report map[string]any) map[string]any { + if report == nil { + return report + } + delete(report, "paths") + if a, ok := report["artifact"].(map[string]any); ok { + delete(a, "path") + } + if m, ok := report["matrix"].(map[string]any); ok { + delete(m, "path") + } + if targets, ok := report["targets"].([]any); ok { + for _, t := range targets { + if tm, ok := t.(map[string]any); ok { + delete(tm, "vm_run_dir") + delete(tm, "qemu_command") + delete(tm, "serial_log") + } + } + } + return report +} diff --git a/internal/api/sanitize_test.go b/internal/api/sanitize_test.go new file mode 100644 index 0000000..d381629 --- /dev/null +++ b/internal/api/sanitize_test.go @@ -0,0 +1,95 @@ +package api + +import ( + "testing" + + "github.com/kernel-guard/bpfcompat/internal/runner" + "github.com/kernel-guard/bpfcompat/pkg/schema" +) + +func sampleInternalReport() schema.ReportV01 { + return schema.ReportV01{ + SchemaVersion: "0.1", + Artifact: schema.Artifact{Path: "/home/azureuser/secret/aegis.bpf.o", BaseName: "aegis", SHA256: "abc"}, + Matrix: schema.MatrixInfo{Path: "/home/azureuser/matrices/mvp.yaml", Name: "mvp"}, + Targets: []schema.Target{{ + ProfileID: "debian-12-6.1", + Status: "pass", + VMRunDir: ".bpfcompat/runs/20260616-abc/vm", + QEMUCommand: "qemu-system-x86_64 -drive file=.bpfcompat/runs/20260616-abc/id_ed25519", + SerialLog: ".bpfcompat/runs/20260616-abc/serial.log", + Validation: &schema.Validation{LoadStatus: "ok"}, + }}, + Paths: schema.Paths{RunDir: ".bpfcompat/runs/20260616-abc", JSON: "/home/azureuser/reports/x.json", Markdown: "/home/azureuser/reports/x.md"}, + } +} + +func TestSanitizeReportForPublicStripsInternalFields(t *testing.T) { + got := sanitizeReportForPublic(sampleInternalReport()) + + if got.Paths != (schema.Paths{}) { + t.Errorf("expected paths cleared, got %+v", got.Paths) + } + if got.Artifact.Path != "" { + t.Errorf("expected artifact path cleared, got %q", got.Artifact.Path) + } + if got.Matrix.Path != "" { + t.Errorf("expected matrix path cleared, got %q", got.Matrix.Path) + } + tg := got.Targets[0] + if tg.VMRunDir != "" || tg.QEMUCommand != "" || tg.SerialLog != "" { + t.Errorf("expected target internals cleared, got vm_run_dir=%q qemu=%q serial=%q", tg.VMRunDir, tg.QEMUCommand, tg.SerialLog) + } + // Compatibility evidence must survive. + if tg.Status != "pass" || tg.Validation == nil || tg.Validation.LoadStatus != "ok" { + t.Errorf("sanitizer dropped compatibility evidence: %+v", tg) + } + if got.Artifact.SHA256 != "abc" || got.Artifact.BaseName != "aegis" { + t.Errorf("sanitizer dropped artifact identity: %+v", got.Artifact) + } +} + +func TestSanitizeReportForPublicDoesNotMutateOriginal(t *testing.T) { + orig := sampleInternalReport() + _ = sanitizeReportForPublic(orig) + if orig.Targets[0].SerialLog == "" || orig.Paths.JSON == "" { + t.Fatal("sanitizer mutated the original report") + } +} + +func TestPublicValidateResponseDropsHostPaths(t *testing.T) { + resp := publicValidateResponse(runner.RunResult{ + ExitCode: 0, + RunDir: ".bpfcompat/runs/20260616-abc", + Report: sampleInternalReport(), + }) + if resp.RunDir != "" || resp.ReportJSONPath != "" || resp.ReportMarkdownPath != "" { + t.Errorf("expected host paths dropped, got run_dir=%q json=%q md=%q", resp.RunDir, resp.ReportJSONPath, resp.ReportMarkdownPath) + } +} + +func TestSanitizeReportMapForPublic(t *testing.T) { + report := map[string]any{ + "paths": map[string]any{"json": "/home/azureuser/x.json"}, + "artifact": map[string]any{"path": "/home/azureuser/a.bpf.o", "sha256": "abc"}, + "matrix": map[string]any{"path": "/home/azureuser/m.yaml", "name": "mvp"}, + "targets": []any{ + map[string]any{"profile_id": "debian-12-6.1", "status": "pass", "vm_run_dir": "x", "qemu_command": "y", "serial_log": "z"}, + }, + } + got := sanitizeReportMapForPublic(report) + + if _, ok := got["paths"]; ok { + t.Error("expected paths removed") + } + if a := got["artifact"].(map[string]any); a["path"] != nil || a["sha256"] != "abc" { + t.Errorf("artifact not sanitized correctly: %+v", a) + } + tg := got["targets"].([]any)[0].(map[string]any) + if tg["vm_run_dir"] != nil || tg["qemu_command"] != nil || tg["serial_log"] != nil { + t.Errorf("target internals not removed: %+v", tg) + } + if tg["status"] != "pass" { + t.Errorf("target status dropped: %+v", tg) + } +} diff --git a/internal/api/server.go b/internal/api/server.go index 219f061..a1614fc 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -338,8 +338,8 @@ type agentDecisionRequest struct { type validateResponse struct { ExitCode int `json:"exit_code"` - RunDir string `json:"run_dir"` - ReportJSONPath string `json:"report_json_path"` + RunDir string `json:"run_dir,omitempty"` + ReportJSONPath string `json:"report_json_path,omitempty"` ReportMarkdownPath string `json:"report_markdown_path,omitempty"` Report interface{} `json:"report"` } @@ -2308,14 +2308,9 @@ func (s *Server) handleValidate(w http.ResponseWriter, r *http.Request) { s.log().Warn("auto-sync registry warning", slog.String("error", err.Error())) } - response := validateResponse{ - ExitCode: result.ExitCode, - RunDir: result.RunDir, - ReportJSONPath: result.Report.Paths.JSON, - ReportMarkdownPath: result.Report.Paths.Markdown, - Report: result.Report, - } - writeJSON(w, http.StatusOK, response) + // Strip host-internal fields (absolute paths, VM run dir, QEMU command, + // serial-log pointers) before returning over HTTP. See sanitize.go. + writeJSON(w, http.StatusOK, publicValidateResponse(result)) } func (s *Server) handleValidateStart(w http.ResponseWriter, r *http.Request) { @@ -2484,13 +2479,8 @@ func (s *Server) runValidateJob(jobID string, cfg runner.Config) { s.log().Warn("auto-sync registry warning", slog.String("error", err.Error())) } - response := &validateResponse{ - ExitCode: result.ExitCode, - RunDir: result.RunDir, - ReportJSONPath: result.Report.Paths.JSON, - ReportMarkdownPath: result.Report.Paths.Markdown, - Report: result.Report, - } + // Strip host-internal fields before this response leaves the host. See sanitize.go. + response := publicValidateResponse(result) s.updateValidateJob(jobID, func(job *validateJob) { job.State = "completed" job.Stage = string(runner.ProgressStageCompleted) diff --git a/internal/vm/qemu_test.go b/internal/vm/qemu_test.go index 995e40e..af975e3 100644 --- a/internal/vm/qemu_test.go +++ b/internal/vm/qemu_test.go @@ -52,7 +52,10 @@ func TestQEMUSystemBinaryForARM64(t *testing.T) { args := buildQEMUArgs(profile, "/tmp/overlay.qcow2", "/tmp/serial.log", 2222, seedDeliveryNoCloudNet, "http://127.0.0.1:8080/", "", "") joined := strings.Join(args, " ") - if !strings.Contains(joined, "-machine virt,accel=kvm") { + // arm64 always uses the "-machine virt,accel=..." form. The accel mode + // (kvm vs tcg) depends on whether the host exposes /dev/kvm, so don't pin + // it here — explicit kvm/tcg coverage lives in TestMachineArgsForAccelFallback. + if !strings.Contains(joined, "-machine virt,accel=") { t.Fatalf("expected arm64 virt machine args: %s", joined) } if strings.Contains(joined, "-enable-kvm") { From 0213be92e4ffba1766cb5957f46f487cdb2448ad Mon Sep 17 00:00:00 2001 From: ErenAri Date: Wed, 17 Jun 2026 02:23:50 +0300 Subject: [PATCH 2/2] Fix gocritic nilValReturn in sanitize.go Co-Authored-By: Claude Opus 4.8 --- internal/api/sanitize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/sanitize.go b/internal/api/sanitize.go index 6affbca..24c2cf6 100644 --- a/internal/api/sanitize.go +++ b/internal/api/sanitize.go @@ -47,7 +47,7 @@ func publicValidateResponse(result runner.RunResult) *validateResponse { // decoded from disk as a generic map (the history run-report endpoint). func sanitizeReportMapForPublic(report map[string]any) map[string]any { if report == nil { - return report + return nil } delete(report, "paths") if a, ok := report["artifact"].(map[string]any); ok {