Files
zero/internal/tools/file_commit_umask_test.go
JATMNandAyush7614 88ec97d563 fix(tools): publish file writes atomically instead of truncating in place (#1214)
* fix(tools): publish file writes atomically instead of truncating in place

Stage replacements before publication so failed writes preserve existing
contents, successful renames detach hard-link siblings, and creates publish
without replacing raced files. Preserve modes and contained symlinks, with
the existing validated in-place fallback when replacement is refused.

Rebuild Ayush7614's complete change from Twigpine/zero#1159 on current main.
The net patch is unchanged; original authorship is retained.

Original-commit: d97d1cf7ae
Original-commit: 9b994c1903
Original-commit: 48f65ca634
Original-PR: https://github.com/Twigpine/zero/pull/1159
Fixes: #1156

* fix(tools): preserve atomic publication when replacement is refused

Keep regular-file preimages and hard-link siblings intact after rename errors. Record published creates even when staging cleanup fails while preserving the cleanup error. Make the permission regression fixture independent of umask.

---------

Co-authored-by: Ayush7614 <[email protected]>
2026-11-09 12:25:24 -08:00

57 lines
2.1 KiB
Go

//go:build unix
package tools
import (
"context"
"os"
"path/filepath"
"syscall"
"testing"
)
// THE PUBLISHED FILE CARRIES THE PERMISSIONS THE PROCESS UMASK PRODUCES, NOT
// THE RAW MODE AN ASSIGNMENT WOULD SET.
//
// The create path opens its staging file with the same exclusive 0o644 create
// this branch of the write path always honored, and the kernel applies the
// process umask to that open. The publish must carry the granted bits — taken
// from the staging file itself — because a chmod never consults the umask: a
// process that requests tight defaults on new files would find every write it
// publishes shared with the world. Under the restrictive umask a plain mode
// assignment would publish 0o644 where the kernel granted 0o600, so the test
// fails before the granted bits are carried.
func TestCreatePublishesTheUmaskedPermissionsOfTheStagedCreate(t *testing.T) {
for name, testCase := range map[string]struct {
umask int
want os.FileMode
}{
"a restrictive umask keeps every stranger out": {umask: 0o077, want: 0o600},
"a permissive umask keeps the readable bits": {umask: 0o022, want: 0o644},
} {
t.Run(name, func(t *testing.T) {
previous := syscall.Umask(testCase.umask)
defer syscall.Umask(previous)
root := t.TempDir()
target := filepath.Join(root, "created.txt")
result := NewScopedWriteFileTool(root, nil).Run(context.Background(), map[string]any{
"path": "created.txt", "content": "zero\n",
})
if result.Status != StatusOK {
t.Fatalf("create status = %s: %s", result.Status, result.Output)
}
info, err := os.Stat(target)
if err != nil {
t.Fatal(err)
}
if got := info.Mode().Perm(); got != testCase.want {
t.Fatalf("published mode = %#o, want the %#o the kernel granted the staging create under this umask; a raw mode assignment would publish %#o and share the file with hands the umask excludes", got, testCase.want, os.FileMode(0o644))
}
if got, readErr := os.ReadFile(target); readErr != nil || string(got) != "zero\n" {
t.Fatalf("published content = %q, err=%v", got, readErr)
}
})
}
}