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 {