From 292686a19a321f34f2b78434756830706e113ce9 Mon Sep 17 00:00:00 2001 From: MengMengCode <227010654+MengMengCode@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:13:35 +0800 Subject: [PATCH] Fix some problems. --- backend/internal/lxc/lxc.go | 89 +++++++++++++++++++++++++++- backend/internal/lxc/lxc_test.go | 45 ++++++++++++++ backend/internal/server/web/.gitkeep | 1 + 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/backend/internal/lxc/lxc.go b/backend/internal/lxc/lxc.go index 63bb1df..2fedbe3 100644 --- a/backend/internal/lxc/lxc.go +++ b/backend/internal/lxc/lxc.go @@ -1017,12 +1017,99 @@ func findSeccompProfile() (string, error) { "/etc/lxc/common.seccomp", } { if _, err := os.Stat(path); err == nil { - return path, nil + return ensureCVE202643499SeccompProfile(path) } } return "", errors.New("required LXC seccomp profile not found") } +const clicdSeccompProfileDir = "/var/lib/clicd/security/seccomp" +const clicdCVE202643499SeccompProfile = clicdSeccompProfileDir + "/lxc-cve-2026-43499.profile" + +var cve202643499FutexSeccompRules = []string{ + "# clicd managed: mitigate CVE-2026-43499 from LXC guests by blocking PI futex operations", + "futex errno 1 [1,0x6,SCMP_CMP_MASKED_EQ,0x7f]", + "futex errno 1 [1,0x7,SCMP_CMP_MASKED_EQ,0x7f]", + "futex errno 1 [1,0x8,SCMP_CMP_MASKED_EQ,0x7f]", + "futex errno 1 [1,0xb,SCMP_CMP_MASKED_EQ,0x7f]", + "futex errno 1 [1,0xc,SCMP_CMP_MASKED_EQ,0x7f]", + "futex errno 1 [1,0xd,SCMP_CMP_MASKED_EQ,0x7f]", +} + +func ensureCVE202643499SeccompProfile(basePath string) (string, error) { + data, err := os.ReadFile(basePath) + if err != nil { + return "", fmt.Errorf("failed to read LXC seccomp profile: %v", err) + } + content := string(data) + if !isLXCVDenylistSeccompProfile(content) { + return "", fmt.Errorf("LXC seccomp profile %s is not a v2 denylist profile; cannot apply CVE-2026-43499 futex mitigation safely", basePath) + } + if err := os.MkdirAll(clicdSeccompProfileDir, 0755); err != nil { + return "", fmt.Errorf("failed to create CLICD seccomp directory: %v", err) + } + hardened := appendMissingSeccompRules(content, cve202643499FutexSeccompRules) + if err := os.WriteFile(clicdCVE202643499SeccompProfile, []byte(hardened), 0644); err != nil { + return "", fmt.Errorf("failed to write CLICD seccomp profile: %v", err) + } + return clicdCVE202643499SeccompProfile, nil +} + +func isLXCVDenylistSeccompProfile(content string) bool { + lines := nonCommentSeccompLines(content) + return len(lines) >= 2 && lines[0] == "2" && isSeccompDenylistPolicy(lines[1]) +} + +func isSeccompDenylistPolicy(line string) bool { + fields := strings.Fields(line) + if len(fields) == 0 { + return false + } + return fields[0] == "denylist" || fields[0] == "blacklist" +} + +func appendMissingSeccompRules(content string, rules []string) string { + trimmed := strings.TrimRight(content, "\r\n") + existing := map[string]bool{} + for _, line := range strings.Split(trimmed, "\n") { + line = strings.TrimSpace(stripSeccompLineComment(line)) + if line != "" { + existing[line] = true + } + } + var builder strings.Builder + builder.WriteString(trimmed) + for _, rule := range rules { + key := strings.TrimSpace(stripSeccompLineComment(rule)) + if key != "" && existing[key] { + continue + } + builder.WriteString("\n") + builder.WriteString(rule) + } + builder.WriteString("\n") + return builder.String() +} + +func nonCommentSeccompLines(content string) []string { + lines := make([]string, 0) + for _, line := range strings.Split(content, "\n") { + line = strings.TrimSpace(stripSeccompLineComment(line)) + if line == "" { + continue + } + lines = append(lines, line) + } + return lines +} + +func stripSeccompLineComment(line string) string { + if idx := strings.Index(line, "#"); idx >= 0 { + return line[:idx] + } + return line +} + func findAppArmorProfile() (string, error) { data, err := os.ReadFile("/sys/kernel/security/apparmor/profiles") if err != nil { diff --git a/backend/internal/lxc/lxc_test.go b/backend/internal/lxc/lxc_test.go index 04545d0..36ebd46 100644 --- a/backend/internal/lxc/lxc_test.go +++ b/backend/internal/lxc/lxc_test.go @@ -88,3 +88,48 @@ func TestSafeRootfsPathRejectsSiblingPrefix(t *testing.T) { t.Fatalf("safeRootfsPath returned %v, want unsafe rootfs path error", err) } } + +func TestIsLXCVDenylistSeccompProfile(t *testing.T) { + tests := []string{` +# base profile +2 +denylist +[all] +open_by_handle_at errno 1 +`, ` +2 +blacklist allow +[all] +open_by_handle_at errno 1 +`} + + for _, profile := range tests { + if !isLXCVDenylistSeccompProfile(profile) { + t.Fatalf("expected v2 denylist profile for\n%s", profile) + } + } + if isLXCVDenylistSeccompProfile("1\nallowlist\n1\n") { + t.Fatal("did not expect v1 allowlist profile") + } +} + +func TestAppendMissingSeccompRulesAddsFutexMitigationOnce(t *testing.T) { + base := "2\ndenylist\n[all]\nopen_by_handle_at errno 1\n" + + once := appendMissingSeccompRules(base, cve202643499FutexSeccompRules) + twice := appendMissingSeccompRules(once, cve202643499FutexSeccompRules) + + for _, want := range []string{ + "futex errno 1 [1,0x6,SCMP_CMP_MASKED_EQ,0x7f]", + "futex errno 1 [1,0xb,SCMP_CMP_MASKED_EQ,0x7f]", + "futex errno 1 [1,0xc,SCMP_CMP_MASKED_EQ,0x7f]", + "futex errno 1 [1,0xd,SCMP_CMP_MASKED_EQ,0x7f]", + } { + if !strings.Contains(once, want) { + t.Fatalf("missing seccomp rule %q in\n%s", want, once) + } + if strings.Count(twice, want) != 1 { + t.Fatalf("rule %q duplicated in\n%s", want, twice) + } + } +} diff --git a/backend/internal/server/web/.gitkeep b/backend/internal/server/web/.gitkeep index e69de29..30259b2 100644 --- a/backend/internal/server/web/.gitkeep +++ b/backend/internal/server/web/.gitkeep @@ -0,0 +1 @@ +