Repository navigation
Windows: rooted directory opens request write access on every ancestor, so non-admin users get "Access is denied" (session recovery, gateway credentials) #718
Description
CompozyOS version
compozy 0.3.0-beta.31 (direct binary). I didn't run beta.32, but the code path below is unchanged on v0.3.0-beta.32 and main @ 88534944a7.
Operating system
Windows (native) — Windows 11 (build 26200)
Area
Core / daemon · Sessions · CLI
ACP agent
Not agent-specific
Model / provider
—
Steps to reproduce
- On Windows, run the daemon as a standard, non-elevated user, with
~/.compozyin the user profile (the default). - Have at least one session pending stop recovery, or use any path that goes through
openOrCreateDirectoryNoFollow, such as the gateway credentials directory. - Start the daemon or run the CLI command.
What happened, and what did you expect?
Directory opens under the user's own profile fail with "Access is denied":
error: daemon: recover pending session stops: session: recover pending stop for sess_…: session: recovery persistence failed: persist recovered stop event for sess_…: store: open session database parent for "C:\Users\<user>\.compozy\sessions\sess_…\events.db": fileutil: open or create directory: Access is denied.: {Access Denied}
A process has requested access to an object, but has not been granted those access rights.
error: cli: secure gateway credentials directory: fileutil: open directory: Access is denied.: {Access Denied}
The target directories are writable by the user. Running the daemon elevated hides the error. But then files under ~/.compozy get created by an elevated process, and the next non-elevated run fails in new ways. That's how I ended up with a daemon I could only start as administrator.
Expected: writing under C:\Users\<user>\.compozy\… works as a standard user.
Cause. openOrCreateDirectoryNoFollow in internal/fileutil/rooted_write_windows.go opens the root with openIntentMutate. It then opens every path component with FILE_OPEN_IF and FILE_GENERIC_READ|FILE_GENERIC_WRITE. Ancestors such as C:\ and C:\Users can't be opened for write by a standard user, so the walk fails before it reaches the user's directory. Existing ancestors only need traversal rights. Write access is only needed on the last component, or on the parent of a component that has to be created.
Patch we are running locally:
--- a/internal/fileutil/rooted_write_windows.go
+++ b/internal/fileutil/rooted_write_windows.go
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"os"
+ "path/filepath"
"unsafe"
"golang.org/x/sys/windows"
@@ -23,19 +24,40 @@ func openOrCreateDirectoryNoFollow(path string, perm os.FileMode) (*os.File, err
if err != nil {
return nil, err
}
- handle, err := openWindowsRoot(root, openIntentMutate)
+ handle, err := openWindowsRoot(root, openIntentRead)
if err != nil {
return nil, err
}
- for _, part := range parts {
+ currentPath := root
+ for index, part := range parts {
+ access := uint32(windows.FILE_GENERIC_READ)
+ if index == len(parts)-1 {
+ access |= windows.FILE_GENERIC_WRITE
+ }
next, err := openWindowsChildWithDisposition(
handle,
part,
entryDirectory,
- windows.FILE_OPEN_IF,
- windows.FILE_GENERIC_READ|windows.FILE_GENERIC_WRITE,
+ windows.FILE_OPEN,
+ access,
)
+ if errors.Is(err, os.ErrNotExist) {
+ // A missing child needs mutation rights on its parent. Existing
+ // ancestors need only traversal rights (not write access to C:\\).
+ mutableParent, parentErr := openNoFollow(currentPath, entryDirectory, openIntentMutate)
+ if parentErr != nil {
+ err = parentErr
+ } else {
+ next, err = openWindowsChildWithDisposition(
+ windows.Handle(mutableParent.Fd()), part, entryDirectory,
+ windows.FILE_CREATE, windows.FILE_GENERIC_READ|windows.FILE_GENERIC_WRITE,
+ )
+ if closeErr := mutableParent.Close(); closeErr != nil {
+ err = errors.Join(err, fmt.Errorf("fileutil: close mutable parent directory: %w", closeErr))
+ }
+ }
+ }
if err != nil {
if closeErr := windows.CloseHandle(handle); closeErr != nil {
return nil, errors.Join(err, fmt.Errorf("fileutil: close parent directory: %w", closeErr))
@@ -52,6 +74,7 @@ func openOrCreateDirectoryNoFollow(path string, perm os.FileMode) (*os.File, err
return nil, fmt.Errorf("fileutil: close parent directory: %w", closeErr)
}
handle = next
+ currentPath = filepath.Join(currentPath, part)
}
return windowsFile(handle, path)The focused internal/fileutil tests for this path pass with the patch. A few unrelated tests in that package fail on Windows without admin rights, because they need symlink privileges. With the patch, the daemon starts and recovers sessions as a standard user.
Logs
(the errors above)
Final checks
- I searched existing issues and this is not a duplicate.
- I can reproduce this on the latest release. (Reproduced on beta.31; on beta.32 and
mainI only confirmed by reading the source.)
Diagnosed with Claude Code on my machine. The diff is the patch I'm running locally, offered as a starting point.
Metadata
Metadata
Assignees
Labels
Type
Fields
Projects
- StatusShow more project fieldsDone
Activity