From f8214a085b9dbad6ee327476ad70603547f36adb Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Thu, 18 Jun 2026 11:51:04 -0300 Subject: [PATCH 1/4] Updated validation for snapshot --- splitio/commitversion.go | 2 +- splitio/proxy/initialization.go | 2 +- splitio/proxy/initialization_test.go | 166 +++++++++++++++++++++++++++ splitio/version.go | 2 +- 4 files changed, 169 insertions(+), 3 deletions(-) diff --git a/splitio/commitversion.go b/splitio/commitversion.go index ad6eb1ba..1490eb6d 100644 --- a/splitio/commitversion.go +++ b/splitio/commitversion.go @@ -5,4 +5,4 @@ This file is created automatically, please do not edit */ // CommitVersion is the version of the last commit previous to release -const CommitVersion = "db1e39d" +const CommitVersion = "c00c925" diff --git a/splitio/proxy/initialization.go b/splitio/proxy/initialization.go index fa3ca325..3b87c222 100644 --- a/splitio/proxy/initialization.go +++ b/splitio/proxy/initialization.go @@ -60,7 +60,7 @@ func Start(logger logging.LoggerInterface, cfg *pconf.Main) error { currentHash := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::")) if snap.Meta().Hash != strconv.Itoa(int(currentHash)) { - return common.NewInitError(errors.New("snapshot cfg (apikey, version, flagsets) does not match the provided one"), common.ExitErrorDB) + logger.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one") } logger.Debug("Database created from snapshot at", dbpath) diff --git a/splitio/proxy/initialization_test.go b/splitio/proxy/initialization_test.go index ed2d6f8c..e5aeb332 100644 --- a/splitio/proxy/initialization_test.go +++ b/splitio/proxy/initialization_test.go @@ -1,11 +1,19 @@ package proxy import ( + "fmt" + "os" + "strconv" + "strings" "sync/atomic" "testing" "time" "github.com/splitio/go-split-commons/v9/synchronizer" + "github.com/splitio/go-toolkit/v5/logging" + "github.com/splitio/split-synchronizer/v5/splitio/common/snapshot" + pconf "github.com/splitio/split-synchronizer/v5/splitio/proxy/conf" + "github.com/splitio/split-synchronizer/v5/splitio/util" ) type syncManagerMock struct { @@ -67,3 +75,161 @@ func TestSyncManagerInitializationRetriesWithSnapshot(t *testing.T) { t.Error("there should be 2 executions") } } + +// mockLogger captures warning messages for testing +type mockLogger struct { + logging.LoggerInterface + warnings []string +} + +func (m *mockLogger) Warning(msg ...interface{}) { + m.warnings = append(m.warnings, fmt.Sprint(msg...)) +} + +func (m *mockLogger) Debug(msg ...interface{}) {} +func (m *mockLogger) Error(msg ...interface{}) {} +func (m *mockLogger) Info(msg ...interface{}) {} + +func TestSnapshotHashMismatchLogsWarning(t *testing.T) { + // Create a temporary snapshot file with a specific hash + tmpDir, err := os.MkdirTemp("", "snapshot-test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + snapshotPath := tmpDir + "/test-snapshot.snap" + + // Create a snapshot with hash "12345" + meta := snapshot.Metadata{ + Version: 1, + Storage: 1, + Hash: "12345", + } + snap, err := snapshot.New(meta, []byte("test data")) + if err != nil { + t.Fatalf("failed to create snapshot: %v", err) + } + + // Encode snapshot to bytes and write to file + encoded, err := snap.Encode() + if err != nil { + t.Fatalf("failed to encode snapshot: %v", err) + } + + if err := os.WriteFile(snapshotPath, encoded, 0644); err != nil { + t.Fatalf("failed to write snapshot file: %v", err) + } + + // Create a config with different apikey/version/flagsets that will produce a different hash + cfg := &pconf.Main{ + Apikey: "test-apikey", + FlagSpecVersion: "1.1", + FlagSetsFilter: []string{"set1", "set2"}, + Initialization: pconf.Initialization{ + Snapshot: snapshotPath, + }, + } + + // Calculate what the hash would be with this config + currentHash := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::")) + expectedHashStr := strconv.Itoa(int(currentHash)) + + // Verify that the hashes are indeed different + if meta.Hash == expectedHashStr { + t.Fatal("test setup error: hashes should be different for this test") + } + + // Create a mock logger to capture warnings + mockLog := &mockLogger{ + warnings: make([]string, 0), + } + + // This simulates the code path in Start() that checks the hash + snap2, err := snapshot.DecodeFromFile(snapshotPath) + if err != nil { + t.Fatalf("failed to decode snapshot: %v", err) + } + + currentHash2 := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::")) + if snap2.Meta().Hash != strconv.Itoa(int(currentHash2)) { + mockLog.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one") + } + + // Verify that a warning was logged + if len(mockLog.warnings) != 1 { + t.Errorf("expected 1 warning, got %d", len(mockLog.warnings)) + } + + expectedWarning := "snapshot cfg (apikey, version, flagsets) does not match the provided one" + if len(mockLog.warnings) > 0 && mockLog.warnings[0] != expectedWarning { + t.Errorf("expected warning '%s', got '%s'", expectedWarning, mockLog.warnings[0]) + } +} + +func TestSnapshotHashMatchNoWarning(t *testing.T) { + // Create a temporary snapshot file + tmpDir, err := os.MkdirTemp("", "snapshot-test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + snapshotPath := tmpDir + "/test-snapshot.snap" + + // Create config + cfg := &pconf.Main{ + Apikey: "test-apikey", + FlagSpecVersion: "1.1", + FlagSetsFilter: []string{"set1", "set2"}, + Initialization: pconf.Initialization{ + Snapshot: snapshotPath, + }, + } + + // Calculate the hash with the config + currentHash := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::")) + hashStr := strconv.Itoa(int(currentHash)) + + // Create a snapshot with the SAME hash + meta := snapshot.Metadata{ + Version: 1, + Storage: 1, + Hash: hashStr, + } + snap, err := snapshot.New(meta, []byte("test data")) + if err != nil { + t.Fatalf("failed to create snapshot: %v", err) + } + + // Encode snapshot to bytes and write to file + encoded, err := snap.Encode() + if err != nil { + t.Fatalf("failed to encode snapshot: %v", err) + } + + if err := os.WriteFile(snapshotPath, encoded, 0644); err != nil { + t.Fatalf("failed to write snapshot file: %v", err) + } + + // Create a mock logger to capture warnings + mockLog := &mockLogger{ + warnings: make([]string, 0), + } + + // This simulates the code path in Start() that checks the hash + snap2, err := snapshot.DecodeFromFile(snapshotPath) + if err != nil { + t.Fatalf("failed to decode snapshot: %v", err) + } + + currentHash2 := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::")) + if snap2.Meta().Hash != strconv.Itoa(int(currentHash2)) { + mockLog.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one") + } + + // Verify that NO warning was logged + if len(mockLog.warnings) != 0 { + t.Errorf("expected 0 warnings, got %d: %v", len(mockLog.warnings), mockLog.warnings) + } +} diff --git a/splitio/version.go b/splitio/version.go index 4c04dea7..ed9425e8 100644 --- a/splitio/version.go +++ b/splitio/version.go @@ -2,4 +2,4 @@ package splitio // Version is the version of this Agent -const Version = "5.12.5" +const Version = "5.12.6" From aeef5f725b799b3ef70687f39ddfb8d8267c73c7 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Thu, 18 Jun 2026 11:55:53 -0300 Subject: [PATCH 2/4] Updated changelog --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 520a3c95..af30b2c7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +5.12.6 (Jun 18, 2026) +- Updated snapshot validation to warn on configuration mismatches instead of blocking proxy initialization. + 5.12.5 (May 29, 2026) - Fixed vulnerabilities (8 Critical, 3 High, 5 Medium, 3 Low): - C: CVE-2026-39830, CVE-2026-39831, CVE-2026-39832, CVE-2026-39833, CVE-2026-39834, CVE-2026-42508, CVE-2026-39821 From acca7a54a4eb6878449da4af546049aba7d7b5b7 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Thu, 18 Jun 2026 14:33:38 -0300 Subject: [PATCH 3/4] Updated initializacion --- splitio/commitversion.go | 2 +- splitio/proxy/initialization.go | 30 +++++++++++++++------------- splitio/proxy/initialization_test.go | 28 +++++++++++++++++++++----- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/splitio/commitversion.go b/splitio/commitversion.go index 1490eb6d..02e55e0d 100644 --- a/splitio/commitversion.go +++ b/splitio/commitversion.go @@ -5,4 +5,4 @@ This file is created automatically, please do not edit */ // CommitVersion is the version of the last commit previous to release -const CommitVersion = "c00c925" +const CommitVersion = "aeef5f7" diff --git a/splitio/proxy/initialization.go b/splitio/proxy/initialization.go index 3b87c222..d9d95002 100644 --- a/splitio/proxy/initialization.go +++ b/splitio/proxy/initialization.go @@ -47,23 +47,25 @@ func Start(logger logging.LoggerInterface, cfg *pconf.Main) error { // Initialization of DB var dbpath = persistent.BoltInMemoryMode + var snapshotValid = false if snapFile := cfg.Initialization.Snapshot; snapFile != "" { snap, err := snapshot.DecodeFromFile(snapFile) if err != nil { return fmt.Errorf("error parsing snapshot file: %w", err) } - dbpath, err = snap.WriteDataToTmpFile() - if err != nil { - return fmt.Errorf("error writing temporary snapshot file: %w", err) - } - currentHash := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::")) if snap.Meta().Hash != strconv.Itoa(int(currentHash)) { - logger.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one") + logger.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one. Ignoring snapshot and starting with empty storage.") + } else { + // Hash matches - use the snapshot + dbpath, err = snap.WriteDataToTmpFile() + if err != nil { + return fmt.Errorf("error writing temporary snapshot file: %w", err) + } + snapshotValid = true + logger.Debug("Database created from snapshot at", dbpath) } - - logger.Debug("Database created from snapshot at", dbpath) } dbInstance, err := persistent.NewBoltWrapper(dbpath, nil) @@ -89,9 +91,9 @@ func Start(logger logging.LoggerInterface, cfg *pconf.Main) error { splitAPI := api.NewSplitAPI(cfg.Apikey, *advanced, logger, metadata) // Proxy storages already implement the observable interface, so no need to wrap them - splitStorage := storage.NewProxySplitStorage(dbInstance, logger, flagsets.NewFlagSetFilter(cfg.FlagSetsFilter), cfg.Initialization.Snapshot != "") - ruleBasedStorage := storage.NewProxyRuleBasedSegmentsStorage(dbInstance, logger, cfg.Initialization.Snapshot != "") - segmentStorage := storage.NewProxySegmentStorage(dbInstance, logger, cfg.Initialization.Snapshot != "") + splitStorage := storage.NewProxySplitStorage(dbInstance, logger, flagsets.NewFlagSetFilter(cfg.FlagSetsFilter), snapshotValid) + ruleBasedStorage := storage.NewProxyRuleBasedSegmentsStorage(dbInstance, logger, snapshotValid) + segmentStorage := storage.NewProxySegmentStorage(dbInstance, logger, snapshotValid) largeSegmentStorage := inmemory.NewLargeSegmentsStorage() // Local telemetry @@ -176,13 +178,13 @@ func Start(logger logging.LoggerInterface, cfg *pconf.Main) error { return common.NewInitError(fmt.Errorf("error instantiating sync manager: %w", err), common.ExitTaskInitialization) } - // Try to start bg sync in BG with unlimited retries (when a snapshot is provided), + // Try to start bg sync in BG with unlimited retries (when a valid snapshot is provided), // the passed function is invoked upon initialization completion - // If no snapshot is provided and init fails, `errUnrecoverable` is returned and application execution is aborted + // If no valid snapshot is provided and init fails, `errUnrecoverable` is returned and application execution is aborted // health monitors are only started after successful init (otherwise they'll fail if the app doesn't sync correctly within the /// specified refresh period) before := time.Now() - err = startBGSync(syncManager, mstatus, cfg.Initialization.Snapshot != "", func() { + err = startBGSync(syncManager, mstatus, snapshotValid, func() { logger.Info("Synchronizer tasks started") appMonitor.Start() servicesMonitor.Start() diff --git a/splitio/proxy/initialization_test.go b/splitio/proxy/initialization_test.go index e5aeb332..52a86a9a 100644 --- a/splitio/proxy/initialization_test.go +++ b/splitio/proxy/initialization_test.go @@ -90,7 +90,7 @@ func (m *mockLogger) Debug(msg ...interface{}) {} func (m *mockLogger) Error(msg ...interface{}) {} func (m *mockLogger) Info(msg ...interface{}) {} -func TestSnapshotHashMismatchLogsWarning(t *testing.T) { +func TestSnapshotHashMismatchLogsWarningAndIgnoresSnapshot(t *testing.T) { // Create a temporary snapshot file with a specific hash tmpDir, err := os.MkdirTemp("", "snapshot-test") if err != nil { @@ -151,9 +151,13 @@ func TestSnapshotHashMismatchLogsWarning(t *testing.T) { t.Fatalf("failed to decode snapshot: %v", err) } + // Simulate the new behavior: check hash and ignore if mismatch + var snapshotValid = false currentHash2 := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::")) if snap2.Meta().Hash != strconv.Itoa(int(currentHash2)) { - mockLog.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one") + mockLog.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one. Ignoring snapshot and starting with empty storage.") + } else { + snapshotValid = true } // Verify that a warning was logged @@ -161,13 +165,18 @@ func TestSnapshotHashMismatchLogsWarning(t *testing.T) { t.Errorf("expected 1 warning, got %d", len(mockLog.warnings)) } - expectedWarning := "snapshot cfg (apikey, version, flagsets) does not match the provided one" + expectedWarning := "snapshot cfg (apikey, version, flagsets) does not match the provided one. Ignoring snapshot and starting with empty storage." if len(mockLog.warnings) > 0 && mockLog.warnings[0] != expectedWarning { t.Errorf("expected warning '%s', got '%s'", expectedWarning, mockLog.warnings[0]) } + + // Verify that snapshot is marked as invalid + if snapshotValid { + t.Error("snapshot should be marked as invalid when hash mismatches") + } } -func TestSnapshotHashMatchNoWarning(t *testing.T) { +func TestSnapshotHashMatchNoWarningAndUsesSnapshot(t *testing.T) { // Create a temporary snapshot file tmpDir, err := os.MkdirTemp("", "snapshot-test") if err != nil { @@ -223,13 +232,22 @@ func TestSnapshotHashMatchNoWarning(t *testing.T) { t.Fatalf("failed to decode snapshot: %v", err) } + // Simulate the new behavior: check hash and use if match + var snapshotValid = false currentHash2 := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::")) if snap2.Meta().Hash != strconv.Itoa(int(currentHash2)) { - mockLog.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one") + mockLog.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one. Ignoring snapshot and starting with empty storage.") + } else { + snapshotValid = true } // Verify that NO warning was logged if len(mockLog.warnings) != 0 { t.Errorf("expected 0 warnings, got %d: %v", len(mockLog.warnings), mockLog.warnings) } + + // Verify that snapshot is marked as valid + if !snapshotValid { + t.Error("snapshot should be marked as valid when hash matches") + } } From 6b78f75486941be3952b49e392b740d21e077476 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Fri, 19 Jun 2026 14:00:28 -0300 Subject: [PATCH 4/4] Updated changelog --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index af30b2c7..aeaa9c62 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -5.12.6 (Jun 18, 2026) +5.12.6 (Jun 19, 2026) - Updated snapshot validation to warn on configuration mismatches instead of blocking proxy initialization. 5.12.5 (May 29, 2026)