From 4600cb70197a54f64b8d131f106128dc0df8c94d Mon Sep 17 00:00:00 2001 From: Jun Matsushita Date: Mon, 30 Mar 2026 22:04:51 +0000 Subject: [PATCH] fix: add mutex to LocalRepositoryCache to prevent concurrent map panic LocalRepositoryCache.CacheDirCache is a plain map[string]string accessed concurrently by Fetch (write) and GetTarArchive (read) when multiple matrix jobs resolve --local-repository actions in parallel. This causes: fatal error: concurrent map read and map write goroutine ... [running]: LocalRepositoryCache.GetTarArchive(...) local_repository_cache.go:47 Add a sync.RWMutex to serialize access to CacheDirCache: - Fetch: write lock when updating the cache - GetTarArchive: read lock when looking up cached paths LocalRepositories is set at initialization and only read after, so it does not need synchronization. This is the same class of bug as #6028 (fixed in #6029 for GoGitActionCache), but in the --local-repository code path. --- pkg/runner/local_repository_cache.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/runner/local_repository_cache.go b/pkg/runner/local_repository_cache.go index 67376a70883..a0524dcb4f3 100644 --- a/pkg/runner/local_repository_cache.go +++ b/pkg/runner/local_repository_cache.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "strings" + "sync" "github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/filecollector" @@ -20,6 +21,7 @@ type LocalRepositoryCache struct { Parent ActionCache LocalRepositories map[string]string CacheDirCache map[string]string + cacheDirMu sync.RWMutex } func (l *LocalRepositoryCache) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) { @@ -27,13 +29,17 @@ func (l *LocalRepositoryCache) Fetch(ctx context.Context, cacheDir, url, ref, to logger.Debugf("LocalRepositoryCache fetch %s with ref %s", url, ref) if dest, ok := l.LocalRepositories[fmt.Sprintf("%s@%s", url, ref)]; ok { logger.Infof("LocalRepositoryCache matched %s with ref %s to %s", url, ref, dest) + l.cacheDirMu.Lock() l.CacheDirCache[fmt.Sprintf("%s@%s", cacheDir, ref)] = dest + l.cacheDirMu.Unlock() return ref, nil } if purl, err := goURL.Parse(url); err == nil { if dest, ok := l.LocalRepositories[fmt.Sprintf("%s@%s", strings.TrimPrefix(purl.Path, "/"), ref)]; ok { logger.Infof("LocalRepositoryCache matched %s with ref %s to %s", url, ref, dest) + l.cacheDirMu.Lock() l.CacheDirCache[fmt.Sprintf("%s@%s", cacheDir, ref)] = dest + l.cacheDirMu.Unlock() return ref, nil } } @@ -44,7 +50,10 @@ func (l *LocalRepositoryCache) Fetch(ctx context.Context, cacheDir, url, ref, to func (l *LocalRepositoryCache) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) { logger := common.Logger(ctx) // sha is mapped to ref in fetch if there is a local override - if dest, ok := l.CacheDirCache[fmt.Sprintf("%s@%s", cacheDir, sha)]; ok { + l.cacheDirMu.RLock() + dest, ok := l.CacheDirCache[fmt.Sprintf("%s@%s", cacheDir, sha)] + l.cacheDirMu.RUnlock() + if ok { logger.Infof("LocalRepositoryCache read cachedir %s with ref %s and subpath '%s' from %s", cacheDir, sha, includePrefix, dest) srcPath := filepath.Join(dest, includePrefix) buf := &bytes.Buffer{}