Skip to content

files.writeFiles owner/group metadata is not applied to auto-created parent directories #1064

@tea-artist

Description

@tea-artist

Summary

When using sandbox.files.writeFiles([{ path, data, owner, group }]) with a nested target path whose parent directories do not exist, the uploaded file receives the requested owner / group, but the auto-created parent directories keep the execd process ownership.

This becomes a problem when execd runs as root while the workload operates as a non-root user: the target file is owned by the requested user, but the parent directories can remain root-owned, so later non-root cleanup such as rm -rf can fail with Permission denied.

Expected behavior

Either:

  1. parent directories auto-created by /files/upload should receive the same owner / group when upload metadata includes them, or
  2. the API / SDK docs should explicitly state that owner / group only applies to the target file, and callers must call createDirectories() first when parent directory ownership matters.

Source evidence

The JavaScript SDK sends owner, group, and mode only as upload metadata and then calls upload:

  • sdks/sandbox/javascript/src/adapters/filesystemAdapter.ts:564-572
async writeFiles(entries: WriteEntry[]): Promise<void> {
  for (const e of entries) {
    const meta: FileMetadata = {
      path: e.path,
      owner: e.owner,
      group: e.group,
      mode: e.mode,
    };
    await this.uploadFile(meta, e.data ?? "");
  }
}

In execd, upload auto-creates the parent directory before writing the file:

  • components/execd/pkg/web/controller/filesystem_upload.go:149-150
targetDir := filepath.Dir(resolvedPath)
if err := os.MkdirAll(targetDir, os.ModePerm); err != nil {

Then upload applies the metadata permission only to the resolved target file path:

  • components/execd/pkg/web/controller/filesystem_upload.go:100-104
  • components/execd/pkg/web/controller/filesystem_upload.go:214-215
if uerr := writeUploadFile(resolvedPath, fileHeader); uerr != nil {
    return uerr
}
return applyUploadPermission(resolvedPath, meta.Permission)
func applyUploadPermission(resolvedPath string, permission model.Permission) *uploadError {
    chmodErr := ChmodFile(resolvedPath, permission)

ChmodFile() operates only on the path passed to it:

  • components/execd/pkg/web/controller/utils.go:60-76
func ChmodFile(file string, perms model.Permission) error {
    abs, err := pathutil.ExpandAbsPath(file)
    // ...
    return SetFileOwnership(abs, perms.Owner, perms.Group)
}

By contrast, /directories is the API that explicitly creates directories with permission objects:

  • specs/execd-api.yaml:884-890

Minimal reproduction

  1. Start a sandbox where execd runs as root and the workload user is non-root, for example agent.
  2. Use the JS SDK to upload a nested file whose parent directories do not exist:
await sandbox.files.writeFiles([
  {
    path: "/home/agent/workspace/project/components/ui/toast.tsx",
    data: "export const ok = true;\n",
    owner: "agent",
    group: "agent",
  },
]);
  1. Inspect ownership:
stat -c '%U:%G %a %n' \
  /home/agent/workspace/project/components \
  /home/agent/workspace/project/components/ui \
  /home/agent/workspace/project/components/ui/toast.tsx

Observed result: the target file is agent:agent, but the auto-created parent directories are owned by the execd process user, e.g. root:root.

  1. Run cleanup as the non-root workload user:
rm -rf /home/agent/workspace/project/components

This can fail with Permission denied because the parent directories are not writable by the workload user.

Why this matters

From the SDK caller's perspective, passing owner / group to writeFiles() appears to make the write safe for that user. In practice, only the leaf file gets fixed, while missing parents can keep root ownership. This is easy to miss until a later replace/delete operation fails.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions