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 <dmitshur@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alex Brainman <alex.brainman@gmail.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
This commit is contained in:
Alex Dubov
2026-05-13 22:59:11 -07:00
committed by Alex Brainman
parent 04396e85d4
commit e4444cbaaa
4 changed files with 342 additions and 1 deletions
+3
View File
@@ -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
+3 -1
View File
@@ -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
+301
View File
@@ -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
}
+35
View File
@@ -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 {