Skip to content

policy: close memoized policy filesystems#3943

Merged
tonistiigi merged 1 commit into
docker:masterfrom
crazy-max:policy-handle-leak
Jul 14, 2026
Merged

policy: close memoized policy filesystems#3943
tonistiigi merged 1 commit into
docker:masterfrom
crazy-max:policy-handle-leak

Conversation

@crazy-max

@crazy-max crazy-max commented Jul 7, 2026

Copy link
Copy Markdown
Member

This fixes policy filesystem cleanup so a policy filesystem returned by newPolicyPathFS releases the backend roots it acquired during one logical use.

On Windows, the old implementation can leave the policy context directory open after loadPolicyData succeeds. This test reproduces the failure:

func TestLoadPolicyDataReleasesPolicyDir(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "policy.rego"), []byte("package docker\n"), 0600))

	provider := newPolicyPathFS(context.Background(), nil, policyOpt{
		ContextDir: dir,
	})

	_, ok, err := loadPolicyData(provider, "policy.rego")
	require.NoError(t, err)
	require.True(t, ok)
	require.NoError(t, os.RemoveAll(dir))
}

The old implementation fails because Windows cannot remove the directory while the policy root is still open.

--- FAIL: TestLoadPolicyDataReleasesPolicyDir (1.79s)
    policy_loader_test.go:106:
        	Error Trace:	...
        	Error:      	Received unexpected error:
        	            	unlinkat ...: The process cannot access the file because it is being used by another process.
        	Test:       	TestLoadPolicyDataReleasesPolicyDir
FAIL

newPolicyPathFS now returns a per-use wrapper around the shared policy path filesystem. That wrapper lazily acquires the cwd and context backends at most once, records the matching release functions, and closes only the backends acquired by that wrapper.

loadPolicyData calls Stat and then ReadFile, and both operations can resolve through the memoized backend filesystem. The old close path only released one memoized reference for the whole logical policy filesystem use, so the backend root could remain open.

@crazy-max crazy-max added this to the v0.36.0 milestone Jul 7, 2026
@crazy-max crazy-max requested a review from tonistiigi July 7, 2026 09:03
@crazy-max crazy-max marked this pull request as ready for review July 7, 2026 09:03

@tonistiigi tonistiigi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get this. The previous function does reference counting. The new one just closes directly, so should be unsafe for multiple users. I also don't see what is left using the previous function.

@crazy-max

crazy-max commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

I don't get this. The previous function does reference counting. The new one just closes directly, so should be unsafe for multiple users. I also don't see what is left using the previous function.

Yes this repro was not the right because it asserted an internal close callback and fails on Linux too. The actual user-visible failure is Windows-specific because the leaked policy root keeps the policy directory open.

Here is a new repro:

func TestLoadPolicyDataReleasesPolicyDir(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "policy.rego"), []byte("package docker\n"), 0600))

	provider := newPolicyPathFS(context.Background(), nil, policyOpt{
		ContextDir: dir,
	})

	_, ok, err := loadPolicyData(provider, "policy.rego")
	require.NoError(t, err)
	require.True(t, ok)
	require.NoError(t, os.RemoveAll(dir))
}

On Windows without this PR, it fails with this output:

--- FAIL: TestLoadPolicyDataReleasesPolicyDir (1.79s)
    policy_loader_test.go:106:
        	Error Trace:	...
        	Error:      	Received unexpected error:
        	            	unlinkat ...: The process cannot access the file because it is being used by another process.
        	Test:       	TestLoadPolicyDataReleasesPolicyDir
FAIL

The path is loadPolicyData, then root.Stat, then fs.ReadFile. Both calls resolve through policyPathFS, so the memoized context filesystem is acquired more than once during a single logical use of the policy filesystem. The old policyPathFS.Close only released one memoized reference, which leaves the directory-backed root open on Windows.

But your point about closeAll being too broad as a general replacement for the ref-counted close is right. I will rework the implementation to preserve the ref-count abstraction more explicitly.

@crazy-max crazy-max force-pushed the policy-handle-leak branch from 5febb34 to 0b92026 Compare July 11, 2026 14:54
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
@crazy-max crazy-max force-pushed the policy-handle-leak branch from 0b92026 to d5c8b88 Compare July 11, 2026 15:00
@crazy-max crazy-max requested a review from tonistiigi July 11, 2026 15:12
@tonistiigi tonistiigi merged commit 19fc65a into docker:master Jul 14, 2026
160 checks passed
@crazy-max crazy-max deleted the policy-handle-leak branch July 14, 2026 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants