From e4444cbaaaf61cecff8e635874066fcd5c841575 Mon Sep 17 00:00:00 2001 From: Alex Dubov Date: Tue, 14 Apr 2026 17:50:14 +1000 Subject: [PATCH] windows: add NtSetEaFile, NtQueryEaFile and NtQueryInformationFile unix package already provides support for working with file extended attributes via Fsetxattr and Fgetxattr syscalls. Windows offers a similar feature implemented by means of NtSetEaFile and NtQueryEaFile syscalls. Those syscalls are provided by this patch. Additionally, NtQueryInformationFile syscall is added. It is required for correct implementation of extended attribute support in library code, as correct invocation of NtQueryEaFile relies on file information returned by said syscall. It also amends the previously accepted NtSetInformationFile syscall. Fixes golang/go#78672 Change-Id: Ia4ba037eb2777ba02a953a57ec1d365aad17cac5 Reviewed-on: https://go-review.googlesource.com/c/sys/+/766800 Reviewed-by: Dmitri Shuralyov LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com Reviewed-by: Alex Brainman Reviewed-by: Cherry Mui --- windows/syscall_windows.go | 3 + windows/types_windows.go | 4 +- windows/xattr_test.go | 301 ++++++++++++++++++++++++++++++++++++ windows/zsyscall_windows.go | 35 +++++ 4 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 windows/xattr_test.go diff --git a/windows/syscall_windows.go b/windows/syscall_windows.go index 453a7b97..9755bca9 100644 --- a/windows/syscall_windows.go +++ b/windows/syscall_windows.go @@ -452,6 +452,7 @@ func NewCallbackCDecl(fn interface{}) uintptr { //sys RtlInitString(destinationString *NTString, sourceString *byte) = ntdll.RtlInitString //sys NtCreateFile(handle *Handle, access uint32, oa *OBJECT_ATTRIBUTES, iosb *IO_STATUS_BLOCK, allocationSize *int64, attributes uint32, share uint32, disposition uint32, options uint32, eabuffer uintptr, ealength uint32) (ntstatus error) = ntdll.NtCreateFile //sys NtCreateNamedPipeFile(pipe *Handle, access uint32, oa *OBJECT_ATTRIBUTES, iosb *IO_STATUS_BLOCK, share uint32, disposition uint32, options uint32, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (ntstatus error) = ntdll.NtCreateNamedPipeFile +//sys NtQueryInformationFile(handle Handle, iosb *IO_STATUS_BLOCK, outBuffer *byte, outBufferLen uint32, class uint32) (ntstatus error) = ntdll.NtQueryInformationFile //sys NtSetInformationFile(handle Handle, iosb *IO_STATUS_BLOCK, inBuffer *byte, inBufferLen uint32, class uint32) (ntstatus error) = ntdll.NtSetInformationFile //sys RtlDosPathNameToNtPathName(dosName *uint16, ntName *NTUnicodeString, ntFileNamePart *uint16, relativeName *RTL_RELATIVE_NAME) (ntstatus error) = ntdll.RtlDosPathNameToNtPathName_U_WithStatus //sys RtlDosPathNameToRelativeNtPathName(dosName *uint16, ntName *NTUnicodeString, ntFileNamePart *uint16, relativeName *RTL_RELATIVE_NAME) (ntstatus error) = ntdll.RtlDosPathNameToRelativeNtPathName_U_WithStatus @@ -460,6 +461,8 @@ func NewCallbackCDecl(fn interface{}) uintptr { //sys NtSetInformationProcess(proc Handle, procInfoClass int32, procInfo unsafe.Pointer, procInfoLen uint32) (ntstatus error) = ntdll.NtSetInformationProcess //sys NtQuerySystemInformation(sysInfoClass int32, sysInfo unsafe.Pointer, sysInfoLen uint32, retLen *uint32) (ntstatus error) = ntdll.NtQuerySystemInformation //sys NtSetSystemInformation(sysInfoClass int32, sysInfo unsafe.Pointer, sysInfoLen uint32) (ntstatus error) = ntdll.NtSetSystemInformation +//sys NtQueryEaFile(handle Handle, iosb *IO_STATUS_BLOCK, outBuffer *byte, outBufferLen uint32, returnSingleEntry bool, eaList *byte, eaListLen uint32, eaIndex *uint32, restartScan bool) (ntstatus error) = ntdll.NtQueryEaFile +//sys NtSetEaFile(handle Handle, iosb *IO_STATUS_BLOCK, inBuffer *byte, inBufferLen uint32) (ntstatus error) = ntdll.NtSetEaFile //sys RtlAddFunctionTable(functionTable *RUNTIME_FUNCTION, entryCount uint32, baseAddress uintptr) (ret bool) = ntdll.RtlAddFunctionTable //sys RtlDeleteFunctionTable(functionTable *RUNTIME_FUNCTION) (ret bool) = ntdll.RtlDeleteFunctionTable diff --git a/windows/types_windows.go b/windows/types_windows.go index d82299e3..d2574a73 100644 --- a/windows/types_windows.go +++ b/windows/types_windows.go @@ -3043,8 +3043,10 @@ const ( ) const ( - // FileInformationClass for NtSetInformationFile + // FileInformationClass for NtSetInformationFile/NtQueryInformationFile, see + // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ne-wdm-_file_information_class FileBasicInformation = 4 + FileEaInformation = 7 FileRenameInformation = 10 FileDispositionInformation = 13 FilePositionInformation = 14 diff --git a/windows/xattr_test.go b/windows/xattr_test.go new file mode 100644 index 00000000..cab9e842 --- /dev/null +++ b/windows/xattr_test.go @@ -0,0 +1,301 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows + +package windows_test + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + "unsafe" + + "golang.org/x/sys/windows" +) + +func TestFdXattr(t *testing.T) { + fp := filepath.Join(t.TempDir(), "test_fd_xattr.txt") + wantContent := "I am an xattr testing file. I will get some xattrs attached." + wantXa := map[string]string{ + "xattr-key-1": "Value for xattr-key-1", + "xattr-key-2": "Also value, but for xattr-key-2", + "xattr-key-3": "xattr-key-3 needs a value too", + "xattr-key-4": "xattr-key-4 never wanted any value but got one anyway", + } + + xattrSet(t, fp, wantContent, wantXa) + + fr, err := os.Open(fp) + if err != nil { + t.Fatalf("Open for read error: %v", err) + } + defer fr.Close() + + haveContent, err := io.ReadAll(fr) + + if err != nil { + t.Fatalf("Read error: %v", err) + } + if string(haveContent) != wantContent { + t.Fatalf("File content mismatch: want %q, have %q", wantContent, string(haveContent)) + } + + haveXa, err := winGetEa(fr) + if err != nil { + t.Fatalf("Windows get EA error: %v", err) + } + + for k, v := range wantXa { + if haveXa[k] != v { + t.Fatalf("XAttr mismatch for key %q: want %q, have %q", k, v, haveXa[k]) + } + } +} + +func xattrSet(t *testing.T, fp string, content string, xa map[string]string) { + fw, err := os.OpenFile(fp, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o600) + if err != nil { + t.Fatalf("Open for write error: %v", err) + } + defer fw.Close() + + _, err = fw.WriteString(content) + if err != nil { + t.Fatalf("Write error: %v", err) + } + + err = winSetEa(fw, xa) + if err != nil { + if err == windows.STATUS_EAS_NOT_SUPPORTED { + t.Skip("filesystem does not support extended attributes, skipping test") + } + t.Fatalf("Windows set EA error: %v", err) + } +} + +// ExtendedAttribute represents a single Windows EA. +type extendedAttribute struct { + Name string + Value []byte + Flags uint8 +} + +type fileFullEaInformation struct { + NextEntryOffset uint32 + Flags uint8 + NameLength uint8 + ValueLength uint16 +} + +var fileFullEaInformationSize = binary.Size(&fileFullEaInformation{}) + +// Windows just cannot keep its hands off letter case +func keyToAttrName(k string) string { + return strings.ToUpper(k) +} + +func attrNameToKey(k string) string { + return strings.ToLower(k) +} + +func winSetEa(f *os.File, xattrs map[string]string) error { + eas := make([]extendedAttribute, 0, len(xattrs)) + + for k, v := range xattrs { + eas = append(eas, extendedAttribute{ + Name: keyToAttrName(k), + Value: []byte(v), + }) + } + + eaBuf, err := encodeExtendedAttributes(eas) + if err != nil { + return err + } + + var iosb windows.IO_STATUS_BLOCK + err = windows.NtSetEaFile( + windows.Handle(f.Fd()), &iosb, &eaBuf[0], uint32(len(eaBuf)), + ) + + if err != nil { + return err + } + + return nil +} + +func winGetEa(f *os.File) (map[string]string, error) { + sz, err := getEaBlockSize(f) + if err != nil || sz == 0 { + return nil, err + } + + var iosb windows.IO_STATUS_BLOCK + eaBuf := make([]byte, int(sz)) + + err = windows.NtQueryEaFile( + windows.Handle(f.Fd()), + &iosb, + &eaBuf[0], + uint32(len(eaBuf)), + false, + nil, + 0, + nil, + true, + ) + + if err != nil { + return nil, err + } + + eas, err := decodeExtendedAttributes(eaBuf) + if err != nil || len(eas) == 0 { + return nil, err + } + + m := make(map[string]string, len(eas)) + for _, ea := range eas { + m[attrNameToKey(ea.Name)] = string(ea.Value) + } + + return m, nil +} + +func getEaBlockSize(f *os.File) (uint, error) { + var iosb windows.IO_STATUS_BLOCK + var rv uint32 + + err := windows.NtQueryInformationFile( + windows.Handle(f.Fd()), + &iosb, + (*byte)(unsafe.Pointer(&rv)), + uint32(unsafe.Sizeof(rv)), + windows.FileEaInformation, + ) + + if err != nil { + return 0, err + } + + return uint(rv), nil +} + +func encodeExtendedAttributes(eas []extendedAttribute) ([]byte, error) { + var buf bytes.Buffer + for i := range eas { + last := false + if i == len(eas)-1 { + last = true + } + + err := writeExtendedAttributes(&buf, &eas[i], last) + if err != nil { + return nil, err + } + } + return buf.Bytes(), nil +} + +func decodeExtendedAttributes(b []byte) (eas []extendedAttribute, err error) { + for len(b) != 0 { + ea, nb, err := parseExtendedAttributes(b) + if err != nil { + return nil, err + } + + eas = append(eas, ea) + b = nb + } + return +} + +func writeExtendedAttributes(buf *bytes.Buffer, ea *extendedAttribute, last bool) error { + if int(uint8(len(ea.Name))) != len(ea.Name) { + return fmt.Errorf( + "Extended attribute name is too long (limited to 255 bytes): name %q, value %q", + ea.Name, string(ea.Value), + ) + } + + if int(uint16(len(ea.Value))) != len(ea.Value) { + return fmt.Errorf( + "Extended attribute value is too long (limited to 65535 bytes): name %q, value %q", + ea.Name, string(ea.Value), + ) + } + + entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value)) + withPadding := (entrySize + 3) &^ 3 + nextOffset := uint32(0) + if !last { + nextOffset = withPadding + } + info := fileFullEaInformation{ + NextEntryOffset: nextOffset, + Flags: ea.Flags, + NameLength: uint8(len(ea.Name)), + ValueLength: uint16(len(ea.Value)), + } + + err := binary.Write(buf, binary.LittleEndian, &info) + if err != nil { + return err + } + + _, err = buf.Write([]byte(ea.Name)) + if err != nil { + return err + } + + err = buf.WriteByte(0) + if err != nil { + return err + } + + _, err = buf.Write(ea.Value) + if err != nil { + return err + } + + _, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize]) + if err != nil { + return err + } + + return nil +} + +func parseExtendedAttributes(b []byte) (ea extendedAttribute, nb []byte, err error) { + var info fileFullEaInformation + err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info) + if err != nil { + return + } + + nameOffset := fileFullEaInformationSize + nameLen := int(info.NameLength) + valueOffset := nameOffset + int(info.NameLength) + 1 + valueLen := int(info.ValueLength) + nextOffset := int(info.NextEntryOffset) + if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) { + err = fmt.Errorf("Invalid extended attribute buffer offset") + return + } + + ea.Name = string(b[nameOffset : nameOffset+nameLen]) + ea.Value = b[valueOffset : valueOffset+valueLen] + ea.Flags = info.Flags + if info.NextEntryOffset != 0 { + nb = b[info.NextEntryOffset:] + } + return +} diff --git a/windows/zsyscall_windows.go b/windows/zsyscall_windows.go index a506ac0f..192d1930 100644 --- a/windows/zsyscall_windows.go +++ b/windows/zsyscall_windows.go @@ -428,8 +428,11 @@ var ( procNetUserGetInfo = modnetapi32.NewProc("NetUserGetInfo") procNtCreateFile = modntdll.NewProc("NtCreateFile") procNtCreateNamedPipeFile = modntdll.NewProc("NtCreateNamedPipeFile") + procNtQueryEaFile = modntdll.NewProc("NtQueryEaFile") + procNtQueryInformationFile = modntdll.NewProc("NtQueryInformationFile") procNtQueryInformationProcess = modntdll.NewProc("NtQueryInformationProcess") procNtQuerySystemInformation = modntdll.NewProc("NtQuerySystemInformation") + procNtSetEaFile = modntdll.NewProc("NtSetEaFile") procNtSetInformationFile = modntdll.NewProc("NtSetInformationFile") procNtSetInformationProcess = modntdll.NewProc("NtSetInformationProcess") procNtSetSystemInformation = modntdll.NewProc("NtSetSystemInformation") @@ -3740,6 +3743,30 @@ func NtCreateNamedPipeFile(pipe *Handle, access uint32, oa *OBJECT_ATTRIBUTES, i return } +func NtQueryEaFile(handle Handle, iosb *IO_STATUS_BLOCK, outBuffer *byte, outBufferLen uint32, returnSingleEntry bool, eaList *byte, eaListLen uint32, eaIndex *uint32, restartScan bool) (ntstatus error) { + var _p0 uint32 + if returnSingleEntry { + _p0 = 1 + } + var _p1 uint32 + if restartScan { + _p1 = 1 + } + r0, _, _ := syscall.SyscallN(procNtQueryEaFile.Addr(), uintptr(handle), uintptr(unsafe.Pointer(iosb)), uintptr(unsafe.Pointer(outBuffer)), uintptr(outBufferLen), uintptr(_p0), uintptr(unsafe.Pointer(eaList)), uintptr(eaListLen), uintptr(unsafe.Pointer(eaIndex)), uintptr(_p1)) + if r0 != 0 { + ntstatus = NTStatus(r0) + } + return +} + +func NtQueryInformationFile(handle Handle, iosb *IO_STATUS_BLOCK, outBuffer *byte, outBufferLen uint32, class uint32) (ntstatus error) { + r0, _, _ := syscall.SyscallN(procNtQueryInformationFile.Addr(), uintptr(handle), uintptr(unsafe.Pointer(iosb)), uintptr(unsafe.Pointer(outBuffer)), uintptr(outBufferLen), uintptr(class)) + if r0 != 0 { + ntstatus = NTStatus(r0) + } + return +} + func NtQueryInformationProcess(proc Handle, procInfoClass int32, procInfo unsafe.Pointer, procInfoLen uint32, retLen *uint32) (ntstatus error) { r0, _, _ := syscall.SyscallN(procNtQueryInformationProcess.Addr(), uintptr(proc), uintptr(procInfoClass), uintptr(procInfo), uintptr(procInfoLen), uintptr(unsafe.Pointer(retLen))) if r0 != 0 { @@ -3756,6 +3783,14 @@ func NtQuerySystemInformation(sysInfoClass int32, sysInfo unsafe.Pointer, sysInf return } +func NtSetEaFile(handle Handle, iosb *IO_STATUS_BLOCK, inBuffer *byte, inBufferLen uint32) (ntstatus error) { + r0, _, _ := syscall.SyscallN(procNtSetEaFile.Addr(), uintptr(handle), uintptr(unsafe.Pointer(iosb)), uintptr(unsafe.Pointer(inBuffer)), uintptr(inBufferLen)) + if r0 != 0 { + ntstatus = NTStatus(r0) + } + return +} + func NtSetInformationFile(handle Handle, iosb *IO_STATUS_BLOCK, inBuffer *byte, inBufferLen uint32, class uint32) (ntstatus error) { r0, _, _ := syscall.SyscallN(procNtSetInformationFile.Addr(), uintptr(handle), uintptr(unsafe.Pointer(iosb)), uintptr(unsafe.Pointer(inBuffer)), uintptr(inBufferLen), uintptr(class)) if r0 != 0 {