Skip to content

Conversation

@AZero13
Copy link
Contributor

@AZero13 AZero13 commented Dec 5, 2025

The if statement appears inverted: setxattr follows simlinks: lsetxattr does not.

Additionally, _extendedAttributes was ignoring simlinks entirely.

Both of these issues have been addressed.

@AZero13
Copy link
Contributor Author

AZero13 commented Dec 8, 2025

@jmschonfeld ping?

@AZero13
Copy link
Contributor Author

AZero13 commented Dec 9, 2025

@jmschonfeld @compnerd ping?

@jmschonfeld
Copy link
Contributor

@jmschonfeld @compnerd ping?

It's on my list to circle back and review when I have a moment - no need to ping 🙂. Will be working my way through the handful of PRs open to review

@jmschonfeld
Copy link
Contributor

@swift-ci please test

Copy link
Member

@compnerd compnerd left a comment

Choose a reason for hiding this comment

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

The code change looks fine, needs fixes for the tests.

@AZero13
Copy link
Contributor Author

AZero13 commented Dec 9, 2025

I fixed it, but also noticed we weren't passing followSymlinks to _extendedAttributes when the test failed on Darwin so. @compnerd

@AZero13 AZero13 force-pushed the mistake branch 3 times, most recently from 406328d to e1179f0 Compare December 9, 2025 21:36
@AZero13 AZero13 requested a review from jmschonfeld December 10, 2025 14:11
@AZero13 AZero13 force-pushed the mistake branch 2 times, most recently from 5505996 to 0c68c6a Compare December 10, 2025 15:36
@AZero13 AZero13 force-pushed the mistake branch 2 times, most recently from bd02eaa to b179951 Compare December 10, 2025 17:44
@AZero13 AZero13 requested a review from jmschonfeld December 10, 2025 17:44
@AZero13 AZero13 force-pushed the mistake branch 3 times, most recently from 8956f2e to 979d225 Compare December 10, 2025 21:57
@AZero13 AZero13 requested a review from jmschonfeld December 10, 2025 21:58
@AZero13 AZero13 changed the title FileManager: lsetxattr and setxattr are swapped Fix FileManager extended attribute symlink handling Dec 10, 2025
@AZero13 AZero13 force-pushed the mistake branch 3 times, most recently from 5267a46 to 0dd4d06 Compare December 10, 2025 22:29
@AZero13 AZero13 changed the title Fix FileManager extended attribute symlink handling Fix FileManager's extended attribute symlink handling Dec 10, 2025
@AZero13 AZero13 force-pushed the mistake branch 2 times, most recently from 037f442 to 9e17700 Compare December 10, 2025 22:35
@AZero13
Copy link
Contributor Author

AZero13 commented Dec 11, 2025

@jmschonfeld Can we please test and merge?

var extendedAttrs: [String : Data] = [:]
var current = keyList.baseAddress!
let end = keyList.baseAddress!.advanced(by: keyList.count)
let end = keyList.baseAddress!.advanced(by: size)
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this was changed in a recent commit (hard to determine with force pushes rather than follow-on commits which are easier to review). Is there a reason you're switching to use the passed in size here rather than using the size of the allocation returned by the allocate function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes — the size variable gets updated by the second listxattr/extattr_list_* call to reflect the actual number of bytes written.

Copy link
Contributor

Choose a reason for hiding this comment

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

In that case if size is potentially different (if the file system changes and the number of attributes are removed) can we add an assertion that size <= keyList.count? In most cases I assume this shouldn't be an issue, but I'm wary that trusting this value could in some edge case inadvertently introduce a buffer overread here if size somehow becomes something larger than our allocation after it is overwritten.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If anything I'd say the opposite is more likely to happen: if keyList.count changes between allocation and read then this can happen.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think I'm fully following - in what situation would keyList.count change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe this is best for another PR. I'll revert it.

if error.code == .featureUnsupported { return }
guard let posix = error.underlying as? POSIXError else { throw error }
#if canImport(Darwin)
guard posix.code == .ENOTSUP || posix.code == .EOPNOTSUPP else { throw error }
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why ENOTSUP is guarded for Darwin only here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because ENOTSUP is not an error code on Linux.

I tried and it didn't compile.

Copy link
Contributor

Choose a reason for hiding this comment

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

ENOTSUP is an error code on Linux as far as I can tell if I'm reading the man pages correctly, but it looks like it has the same value as EOPNOTSUPP so there's no distinction. It looks like we also don't have a POSIXErrorCode.EOPNOTSUP for it which is likely the compilation error you were seeing (either intentionally because it's not necessary or just because nobody has ended up needing it). If that's the case, can you leave a comment about why this distinction is here so it's clear that ENOTSUP is not distinct from EOPNOTSUPP on non-Darwin platforms?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@AZero13 AZero13 requested a review from jmschonfeld December 11, 2025 19:10
@AZero13
Copy link
Contributor Author

AZero13 commented Dec 11, 2025

I think this is as heavy as change now as it should get. We should merge this

I'll save the count and size replacement for another PR. That has been reverted back @jmschonfeld

@jmschonfeld
Copy link
Contributor

@swift-ci test

@AZero13
Copy link
Contributor Author

AZero13 commented Dec 12, 2025

Test failed because of a permission error on linux. @jmschonfeld what to do? How am I supposed to fix that in the test

@AZero13 AZero13 force-pushed the mistake branch 4 times, most recently from fd46df6 to 3adb585 Compare December 12, 2025 18:26
@AZero13
Copy link
Contributor Author

AZero13 commented Dec 12, 2025

Ugh I had to fix the tests to use rawvalue. The previous version passed locally, but not on the CI. My bad...

if error.code == .featureUnsupported { return }
guard let posix = error.underlying as? POSIXError else { throw error }
#if canImport(Darwin)
guard posix.code == .ENOTSUP || posix.code.rawValue == EOPNOTSUPP || posix.code == .EPERM else { throw error }
Copy link
Contributor

Choose a reason for hiding this comment

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

On the EPERM issue - yes this seems like it somewhat defeats the purpose of the test, since if permission is always denied then we're always skipping this test effectively. Have you investigated why the EPERM error is occurring or do we know why we're getting this error? Perhaps there's something about the test file that should be updated to allow this operation, or does this signify that this operation in general isn't the right direction?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I think we should just use the raw values. That is what I did.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How about having it just for the probe though?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm referring to the need to check for EPERM at all here. Do you know why we're failing with an EPERM error when trying to set these attributes? I'd like to better understand in which situations this is actually possible and which situations it is expected to fail for one reason or another. We need to be careful here to make sure that introducing this change does not cause compatibility issues with clients expecting these calls to succeed if the calls begin to fail because the operation is no longer possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The operation itself is not reliably possible across platforms/filesystems.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is the CI running in a sandbox?

Copy link
Contributor

Choose a reason for hiding this comment

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

they already can fail with EPERM depending on filesystem/platform because the implementation calls lsetxattr() for symlinks.

Right what I'm driving towards is figuring out why the can fail with that message. Is it similar to the EOPNOTSUPP case where the file system or operating system does not permit the operation on a symlink, and if so which are those? We've already excluded some operating systems for this reason, are there more? Is it a misconfiguration in the FileManagerPlayground setup that creates the files incorrectly? The problem with ignoring EPERM without understanding why is that we could be simply avoiding testing this behavior across all platforms leaving this change untested and I want to confirm where we expect this to succeed and ensure that tests are running there.

Is the CI running in a sandbox?

I'm not certain - it's possible that it is, or possible that it varies based on platform. That being said, the process running the tests creates the test files themselves and I don't think a sandbox would permit us to create files that we later would be unable to access but I have not validated that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We still check that the target is untouched, which is the core behavioral guarantee. We are not disabling the test across all platforms.

Copy link
Contributor

@jmschonfeld jmschonfeld Dec 16, 2025

Choose a reason for hiding this comment

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

That's not the behavior of the code as-is today. I think the checks for the various error codes are complicating the test here a bit. I discussed with @kperryua and @jrflat and after some research and experimentation we determined that:

  • The EOPNOTSUPP shouldn't be relevant here as it's only relevant for usage with sockets
  • The ENOTSUP shouldn't happen here, since this test is only running on platforms where this operation should be supported
  • The EPERM error seems a bit weird at first, but it seems to be an artifact of the constraint that Linux does not support user-defined xattrs on symlinks themselves

Based on the above, I think we should update the unit test to:

  • No longer account for EOPNOTSUPP and ENOTSUP as they are not relevant here
  • Test that we fail due to the EPERM error only on Linux
  • Still validate the reading side even if the setting side fails

I took a moment to write up my suggestion for how to write this unit test based on the above plus cleaning it up just a little bit to simplify the branches and rename it to match what we're testing:

    // Extended attributes are not supported on all platforms
    #if !os(Windows) && !os(WASI) && !os(OpenBSD) && !canImport(Android)
    @Test func extendedAttributesOnSymlinks() async throws {
        let xattrKey = FileAttributeKey("NSFileExtendedAttributes")
        #if os(Linux)
        // Linux requires the user.* namespace prefix for regular files
        let attrName = "user.swift.foundation.symlinktest"
        #else
        let attrName = "org.swift.foundation.symlinktest"
        #endif
        let attrValue = Data([0xAA, 0xBB, 0xCC])
        let attrValue2 = Data([0xDD, 0xEE, 0xFF])

        try await FilePlayground {
            File("target", contents: Data("payload".utf8))
            SymbolicLink("link", destination: "target")
        }.test { fileManager in
            // First, validate that reading the attribute from the link does not read the value from the target
            do {
                try fileManager.setAttributes([xattrKey: [attrName: attrValue]], ofItemAtPath: "target")
                let targetAttrs = try fileManager.attributesOfItem(atPath: "target")
                let targetXAttrs = targetAttrs[xattrKey] as? [String: Data]
                #expect(targetXAttrs?[attrName] == attrValue)
                
                let linkAttrs = try fileManager.attributesOfItem(atPath: "link")
                let linkXAttrs = linkAttrs[xattrKey] as? [String: Data]
                #expect(linkXAttrs?[attrName] == nil)
            }

            // Attempt to set xattrs on the symlink
            #if os(Linux)
            // On Linux, user xattrs cannot be set on symlinks
            #expect(throws: CocoaError.self) {
                try fileManager.setAttributes([xattrKey: [attrName: attrValue2]], ofItemAtPath: "link")
            }
            let expectedValue: Data? = nil
            #else
            try fileManager.setAttributes([xattrKey: [attrName: attrValue2]], ofItemAtPath: "link")
            let expectedValue: Data? = attrValue2
            #endif
            
            // Ensure that reading back the xattr of the link produces the expected value
            let linkAttrs = try fileManager.attributesOfItem(atPath: "link")
            let linkXAttrs = linkAttrs[xattrKey] as? [String: Data]
            #expect(linkXAttrs?[attrName] == expectedValue)
            
            // Ensure that setting the xattr on the link did not set the xattr on the target
            let targetAttrs = try fileManager.attributesOfItem(atPath: "target")
            let targetXAttrs = targetAttrs[xattrKey] as? [String: Data]
            #expect(targetXAttrs?[attrName] == attrValue)
        }
    }
    #endif

What do you think about that? That way we always test that the reading side reads from the symlink and not the target and on the setting side it fails on Linux but otherwise succeeds and that on all platforms writing to the symlink doesn't write over the target. Does that unit test sound good to you?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes!

The if statement appears inverted:  setxattr follows simlinks: lsetxattr does not.

Additionally, _extendedAttributes was ignoring simlinks entirely.

Both of these issues have been addressed.
@jmschonfeld
Copy link
Contributor

@swift-ci please test

@AZero13
Copy link
Contributor Author

AZero13 commented Dec 16, 2025

It passed @jmschonfeld

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.

3 participants