Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Getter Setters for Extended Attributes and Reparse points. #229

Closed
wants to merge 1 commit into from

Conversation

ambarve
Copy link
Contributor

@ambarve ambarve commented Jan 10, 2022

This PR adds the Go wrappers for getting / setting the extended file attributes and reparse points. This is required for preparing the container scratch directly from hcsshim instead of calling into existing OS APIs.

@ambarve ambarve requested a review from a team as a code owner January 10, 2022 03:14
reparse.go Outdated Show resolved Hide resolved
ea.go Outdated Show resolved Hide resolved
ea.go Outdated Show resolved Hide resolved
ea.go Outdated
return GetFileEAHandle(windows.Handle(f.Fd()))
}

func GetFileEAHandle(handle windows.Handle) ([]ExtendedAttribute, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the way the *Handle methods are named it seems like I'd expect to get back some handle to the EA's themselves which doesn't make sense (guess this is the way my brain works haha). Is exposing string and handle equivalents just to make this as flexible as possible?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need the shortcut methods that take a file path? It's probably not too onerous to expect callers to pass a handle.

Copy link
Contributor

@dcantah dcantah Jan 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I think dropping the Handle off the names and having these be the only functions makes sense to me at least. Not the only function we have in this project that just takes in a handle and has no shortcut string equivalent https://github.com/microsoft/go-winio/blob/master/file.go#L113

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the method which takes the file path mainly for convenience and also so that we can encapsulate correct file opening permissions (for example, you need to open the file with specific access right when gettting/setting EA, or when gettting/setting reparse points). However, there are times when you already have a handle open for a file and you just want to pass in that (probably because you opened it in non sharing mode) that's why I added the other method. I agree that the handle methods can be named better but I think keeping both can be helpful. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevpar / @dcantah any thoughts on this?

Copy link
Contributor

@msscotb msscotb Jan 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If our use case for these is to open a handle, and then query SDs, EAs and reparse points it makes sense to just have the NtAPI style that take handles. Otherwise there is a separate createfile on each function call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for reparse points its best to have the functions that take file paths but for EA we mostly deal with the handles so I have kept handles there. For SDs there is no particular preference but the wrapper from golang.org/x/sys/windows already provides a function which takes in the handle so I kept the file path version here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this comment a bit more? Why is it best to take paths for reparsepoint functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only said that because in order to get handle to a reparse point you need to open it with the FLAG_OPEN_REPARSE_POINT flag so its better to do that in the call that gets or sets the reparse points than expecting the caller to do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@msscotb , @kevpar , @dcantah I have updated the PR, with the latest set of changes getters/setters for EAs take a file handle, getter for SDDL takes a path and getter for reparse takes a path but setter for reparse takes a file handle. The reason for this is that a reparse point could be created on a normal file or a directory so I have left the job of opening the handle to it on the caller. Let me know if this looks good.

ea.go Outdated Show resolved Hide resolved
ea_test.go Show resolved Hide resolved
@dcantah dcantah self-assigned this Jan 10, 2022
reparse_test.go Outdated Show resolved Hide resolved
@dcantah
Copy link
Contributor

dcantah commented Jan 10, 2022

What will this be used for? A PR description and commit descriptions will be helpful

ea.go Outdated Show resolved Hide resolved
reparse.go Outdated Show resolved Hide resolved
reparse.go Outdated Show resolved Hide resolved
reparse.go Outdated Show resolved Hide resolved
ea.go Outdated Show resolved Hide resolved
ea.go Outdated Show resolved Hide resolved
ea.go Outdated Show resolved Hide resolved
ea.go Outdated
return GetFileEAHandle(windows.Handle(f.Fd()))
}

func GetFileEAHandle(handle windows.Handle) ([]ExtendedAttribute, error) {
Copy link
Contributor

@msscotb msscotb Jan 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If our use case for these is to open a handle, and then query SDs, EAs and reparse points it makes sense to just have the NtAPI style that take handles. Otherwise there is a separate createfile on each function call.

ea.go Show resolved Hide resolved
ea.go Outdated Show resolved Hide resolved
ea.go Show resolved Hide resolved
ea.go Outdated Show resolved Hide resolved
sd_test.go Outdated Show resolved Hide resolved
fileinfo.go Outdated Show resolved Hide resolved
@dcantah
Copy link
Contributor

dcantah commented May 5, 2022

I almost forgot we have a Linux CI here now haha, but it's not happy with this PR apparently.

@ambarve ambarve force-pushed the get_set_ea branch 2 times, most recently from 8deb21d to 7755641 Compare May 11, 2022 14:31
@ambarve
Copy link
Contributor Author

ambarve commented May 11, 2022

I almost forgot we have a Linux CI here now haha, but it's not happy with this PR apparently.

Why do we need Linux CI for this repo?

@dcantah
Copy link
Contributor

dcantah commented May 13, 2022

I almost forgot we have a Linux CI here now haha, but it's not happy with this PR apparently.

Why do we need Linux CI for this repo?

There's some code that can run on linux, so probably for sanity #241. cc @helsaawy

@helsaawy
Copy link
Contributor

Why do we need Linux CI for this repo?

@ambarve
hcsshim uses guid.GUID, but since it was restricted to windows , we cant share schema (not that we use schema1 much) and protocol definitions with the guest.
So I added the Linux CI to make sure that anything we expose to Linux works

fileinfo.go Outdated Show resolved Hide resolved
sd.go Outdated Show resolved Hide resolved
reparse.go Outdated Show resolved Hide resolved

// SetReparsePoint sets a reparse point with `data` on the file/directory represented by
// `handle` . `data` should be an encoded ReparseDataBuffer struct.
func SetReparsePoint(handle windows.Handle, data []byte) error {
Copy link
Contributor

@dcantah dcantah Jun 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to make these "friendly" APIs and just take in ReparseDataBuffers directly and we encode inside?

e.g.

func SetReparsePoint(handle windows.Handle, data *ReparseDataBuffer) error {
    dataBuf := data.Encode() 
    // rest of the function
}

and if folks want to call encode/decode and pass around byte slices themselves we leave those methods exported?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC the problem here is that we currently have two structs representing the reparse data buffer in this code. One is the ReparsePoint struct and other is the newly added ReparseDataBuffer struct. The ReparsePoint struct only supports symlinks and mount points while ReparseDataBuffer supports all kinds of reparse points. We have to keep the ReparsePoint struct for backwards compatibility. We want to use the SetReparsePoint call for a buffer that could be an encoded buffer of ReparseDataBuffer or ReparsePoint, hence the []byte array. Let me know what you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the bit that confused me is the comment, it states that it should be an encoded ReparseDataBuffer explicitly, so I thought this didn't care about the old definition

@dcantah
Copy link
Contributor

dcantah commented Jul 22, 2022

@ambarve We were looking to get out a new winio release soon for Hamza's hvsocket changes/fixes, lemme know if you think we'd be able to get this in

} else {
return nil, fmt.Errorf("get file EA failed with: %w", status.Err())
}
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need the else

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the else with break otherwise the for loop doesn't end and the code gets stuck in a loop.
Existing code is correct.

Copy link
Contributor

@dcantah dcantah Aug 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should've been more clear. Keep the break just don't need the else. But we'd need something to retry the loop so we'd need a continue after buf = make([]byte, bufLen), e.g. the below. This seems mostly preference, but in my jumbled brain it flows a bit better

for {
    status := getFileEA(handle, &iosb, &buf[0], uint32(bufLen), false, 0, 0, nil, true)
    if status.Err() != nil {
	// convert ntstatus code to windows error
	if status.Err() == windows.ERROR_INSUFFICIENT_BUFFER || status.Err() == windows.ERROR_MORE_DATA {
		bufLen *= 2
		buf = make([]byte, bufLen)
                continue
	} 
        return nil, fmt.Errorf("get file EA failed with: %w", status.Err())
   }
   break
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean, that does seem a bit more concise.

Comment on lines +184 to +209
func GetReparsePoint(path string) ([]byte, error) {
utf16DestPath, err := windows.UTF16FromString(path)
if err != nil {
return nil, err
}
// We need to open the file with windows specific permissions so just using
// os.Open won't work.
fileHandle, err := windows.CreateFile(&utf16DestPath[0], windows.GENERIC_READ, 0, nil, windows.OPEN_EXISTING, (windows.FILE_ATTRIBUTE_NORMAL | windows.FILE_FLAG_OPEN_REPARSE_POINT | windows.FILE_FLAG_BACKUP_SEMANTICS), 0)
if err != nil {
return nil, fmt.Errorf("open file failed with: %w", err)
}
defer windows.Close(fileHandle)

outBuf := make([]byte, _MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
var outBufLen uint32
err = windows.DeviceIoControl(fileHandle, _FSCTL_GET_REPARSE_POINT, nil, 0, &outBuf[0], _MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &outBufLen, nil)
if err != nil {
return nil, fmt.Errorf("failed to get reparse point for file %s: %w", path, err)
}

return outBuf[:outBufLen], nil
}

// SetReparsePoint sets a reparse point with `data` on the file/directory represented by
// `handle` . `data` should be an encoded ReparseDataBuffer struct.
func SetReparsePoint(handle windows.Handle, data []byte) error {
Copy link
Contributor

@dcantah dcantah Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think the split here in API between Get taking in a path and Set using a handle is strange. Maybe we expose a method to call CreateFile with the minimum set of flags get/set need for the reparse operations to work and make them tweakable. I'd have to look more on what's needed..

@dcantah dcantah mentioned this pull request Aug 19, 2022
// keep increasing the buffer size until it is large enough
for {
status := getFileEA(handle, &iosb, &buf[0], uint32(bufLen), false, 0, 0, nil, true)
if status.Err() != nil {
Copy link

@aneesh-n aneesh-n Aug 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this contribution @ambarve and I'm thankful to all the contributors of this project, it's been very helpful.

I think one aspect that could be improved in this function is that if you pass a handle to a file which does not have any extended attributes, then it returns an error, "get file EA failed with: The file or directory is corrupted and unreadable."
However, this should ideally be a normal situation that should be handled gracefully.
I noticed that the system call returns -1073741742 status code for both files and directories in this case.

Suggested change -

status := getFileEA(handle, &iosb, &buf[0], uint32(bufLen), false, 0, 0, nil, true)

		if status == -1073741742 {
			//If status is -1073741742, no extended attributes were found
			return nil, nil
		} else {
			err := status.Err()
			if err != nil {
				// convert ntstatus code to windows error
				if err == windows.ERROR_INSUFFICIENT_BUFFER || err == windows.ERROR_MORE_DATA {
					bufLen *= 2
					buf = make([]byte, bufLen)
					continue
				}
				return nil, fmt.Errorf("get file EA failed with: %w", err)
			}
		}
		break

That said, I do not know if this is the best way to handle this scenario or if that status code will always be consistent.
Anyone with a more comprehensive understanding of the system internals have any better suggestions?

t.Logf("expected: %+v, found: %+v\n", testEAs, readEAs)
t.Fatalf("EAs read from testfile don't match")
}
}
Copy link

@aneesh-n aneesh-n Aug 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another useful test would be for setting and getting extended attributes for folders.
The main change required is the passing of an extra attribute - windows.FILE_FLAG_BACKUP_SEMANTICS for getting the file handle.


func Test_SetGetFolderEA(t *testing.T) {
	tempDir := t.TempDir()
	testfolderPath := filepath.Join(tempDir, "testfolder")
	// create temp folder
	err := os.Mkdir(testfolderPath, os.ModeDir)
	if err != nil {
		t.Fatalf("failed to create temporary file: %s", err)
	}

	nAttrs := 3
	testEAs := make([]ExtendedAttribute, 3)
	// generate random extended attributes for test
	for i := 0; i < nAttrs; i++ {
		// EA name is automatically converted to upper case before storing, so
		// when reading it back it returns the upper case name. To avoid test
		// failures because of that keep the name upper cased.
		testEAs[i].Name = fmt.Sprintf("TESTEA%d", i+1)
		testEAs[i].Value = make([]byte, rand.Int31n(math.MaxUint8))
		rand.Read(testEAs[i].Value)
	}

	utf16Path := windows.StringToUTF16Ptr(testfolderPath)
	fileAccessRightReadWriteEA := (0x8 | 0x10)
	fileHandle, err := windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS, 0)

	if err != nil {
		t.Fatalf("open folder failed with: %s", err)
	}
	defer windows.Close(fileHandle)

	if err := SetFileEA(fileHandle, testEAs); err != nil {
		t.Fatalf("set EA for folder failed: %s", err)
	}

	var readEAs []ExtendedAttribute
	if readEAs, err = GetFileEA(fileHandle); err != nil {
		t.Fatalf("get EA for folder failed: %s", err)
	}

	if !reflect.DeepEqual(readEAs, testEAs) {
		t.Logf("expected: %+v, found: %+v\n", testEAs, readEAs)
		t.Fatalf("EAs read from test folder don't match")
	}
}

@msscotb msscotb assigned msscotb and unassigned dcantah May 31, 2023
@ambarve
Copy link
Contributor Author

ambarve commented Jul 19, 2023

These changes are not required any more so closing this out. Will be reopened later if required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants