From 440e605123a22748038114cbf4a202950466d9c9 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Tue, 5 Aug 2025 13:50:03 +0300 Subject: [PATCH 1/3] fix(filterlists): Resolve occasional FilterLists initialization failures Improved module initialization and processing logic to enhance concurrency handling and ensure reliable https://github.com/safing/portmaster/issues/1951 --- service/intel/filterlists/database.go | 11 +++++++++++ service/intel/filterlists/module.go | 12 ++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/service/intel/filterlists/database.go b/service/intel/filterlists/database.go index 7653a5f4..296ac347 100644 --- a/service/intel/filterlists/database.go +++ b/service/intel/filterlists/database.go @@ -110,6 +110,7 @@ func processListFile(ctx context.Context, filter *scopedBloom, file *updates.Art }) } + // Decode file and send entries to values channel. startSafe(func() (err error) { defer close(values) @@ -117,6 +118,15 @@ func processListFile(ctx context.Context, filter *scopedBloom, file *updates.Art return }) + // Wait for the module initialization to complete to ensure the filter is fully loaded. + // This avoids prolonged locks during filter updates caused by concurrent database loading. + select { + case <-moduleInitDone: + case <-time.After(time.Second * 20): + log.Warning("intel/filterlists: timeout waiting for module initialization") + } + + // Process each entry and send records to records channel. startSafe(func() error { defer close(records) for entry := range values { @@ -128,6 +138,7 @@ func processListFile(ctx context.Context, filter *scopedBloom, file *updates.Art return nil }) + // Persist records to the database. persistRecords(startSafe, records) return g.Wait() diff --git a/service/intel/filterlists/module.go b/service/intel/filterlists/module.go index fd780890..f2780e5f 100644 --- a/service/intel/filterlists/module.go +++ b/service/intel/filterlists/module.go @@ -45,14 +45,17 @@ func (fl *FilterLists) Stop() error { return stop() } -// booleans mainly used to decouple the module -// during testing. var ( + moduleInitDone chan struct{} + + // booleans mainly used to decouple the module during testing. ignoreUpdateEvents = abool.New() ignoreNetEnvEvents = abool.New() ) func init() { + moduleInitDone = make(chan struct{}) + ignoreNetEnvEvents.Set() } @@ -87,6 +90,10 @@ func start() error { filterListLock.Lock() defer filterListLock.Unlock() + // Signal that the module has been initialized. + // This indicates that the module is ready for use, with the default filter + defer close(moduleInitDone) + ver, err := getCacheDatabaseVersion() if err == nil { log.Debugf("intel/filterlists: cache database has version %s", ver.String()) @@ -108,6 +115,7 @@ func start() error { } func stop() error { + moduleInitDone = make(chan struct{}) filterListsLoaded = make(chan struct{}) return nil } From 645438bbf468a8cf26a866ab2ef077821164dbdd Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Tue, 5 Aug 2025 15:33:14 +0300 Subject: [PATCH 2/3] fix(filterlists): Semantic version comparison for filter lists index file Avoid false positives, like: "2025.04.14" != "2025.4.14" --- service/intel/filterlists/index.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/service/intel/filterlists/index.go b/service/intel/filterlists/index.go index 8293f84c..042be684 100644 --- a/service/intel/filterlists/index.go +++ b/service/intel/filterlists/index.go @@ -7,6 +7,7 @@ import ( "strings" "sync" + "github.com/hashicorp/go-version" "github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database/record" "github.com/safing/portmaster/base/log" @@ -167,6 +168,16 @@ var ( listIndexUpdateLock sync.Mutex ) +// Compares two version strings and returns true only if both are successfully parsed and equal +func areSemversEqual(v1, v2 string) bool { + parsedV1, err1 := version.NewSemver(v1) + parsedV2, err2 := version.NewSemver(v2) + if err1 != nil || err2 != nil { + return false + } + return parsedV1.Equal(parsedV2) +} + func updateListIndex() error { listIndexUpdateLock.Lock() defer listIndexUpdateLock.Unlock() @@ -188,7 +199,9 @@ func updateListIndex() error { log.Info("filterlists: index not in cache, starting update") case err != nil: log.Warningf("filterlists: failed to load index from cache, starting update: %s", err) - case listIndexUpdate.Version != strings.TrimPrefix(index.Version, "v"): + case listIndexUpdate.Version != strings.TrimPrefix(index.Version, "v") && + // Avoid false positives by checking if the version is actually different (e.g. "2025.04.14 == 2025.4.14") + !areSemversEqual(listIndexUpdate.Version, index.Version): log.Infof( "filterlists: index from cache is outdated, starting update (%s != %s)", strings.TrimPrefix(index.Version, "v"), From 878de4ba9ddb3bf6af6420e22caa5b6b19ece31d Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Tue, 5 Aug 2025 15:40:02 +0300 Subject: [PATCH 3/3] refactor(filterlists): Improve index update logging feat(updates): Add String method --- service/intel/filterlists/index.go | 2 +- service/intel/filterlists/updater.go | 2 +- service/updates/index.go | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/service/intel/filterlists/index.go b/service/intel/filterlists/index.go index 042be684..d50250a4 100644 --- a/service/intel/filterlists/index.go +++ b/service/intel/filterlists/index.go @@ -209,7 +209,7 @@ func updateListIndex() error { ) default: // List is in cache and current, there is nothing to do. - log.Debug("filterlists: index is up to date") + log.Debugf("filterlists: index is up to date (%s == %s)", index.Version, listIndexUpdate.Version) // Update the unbreak filter list IDs on initial load. updateUnbreakFilterListIDs() diff --git a/service/intel/filterlists/updater.go b/service/intel/filterlists/updater.go index 53227264..d404b8c0 100644 --- a/service/intel/filterlists/updater.go +++ b/service/intel/filterlists/updater.go @@ -238,7 +238,7 @@ func getUpgradableFiles() ([]*updates.Artifact, error) { func resolveUpdateOrder(updateOrder []*updates.Artifact) ([]*updates.Artifact, error) { // sort the update order by ascending version sort.Sort(byAscVersion(updateOrder)) - log.Tracef("intel/filterlists: order of updates: %v", updateOrder) + log.Tracef("intel/filterlists: order of potential updates: %v", updateOrder) var cacheDBVersion *version.Version if !isLoaded() { diff --git a/service/updates/index.go b/service/updates/index.go index 63546012..5b143538 100644 --- a/service/updates/index.go +++ b/service/updates/index.go @@ -59,6 +59,10 @@ func (a *Artifact) SemVer() *semver.Version { return a.versionNum } +func (a *Artifact) String() string { + return fmt.Sprintf("%s(v%s)", a.Filename, a.Version) +} + // IsNewerThan returns whether the artifact is newer than the given artifact. // Returns true if the given artifact is nil. // The second return value "ok" is false when version could not be compared.