diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift index b2666ccbc..a341652f7 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -522,21 +522,21 @@ extension _FileManagerImpl { private func _extendedAttributes(at path: UnsafePointer, followSymlinks: Bool) throws -> [String : Data]? { #if canImport(Darwin) - var size = listxattr(path, nil, 0, 0) + var size = listxattr(path, nil, 0, followSymlinks ? 0 : XATTR_NOFOLLOW) #elseif os(FreeBSD) var size = (followSymlinks ? extattr_list_file : extattr_list_link)(path, EXTATTR_NAMESPACE_USER, nil, 0) #else - var size = listxattr(path, nil, 0) + var size = followSymlinks ? listxattr(path, nil, 0) : llistxattr(path, nil, 0) #endif guard size > 0 else { return nil } let keyList = UnsafeMutableBufferPointer.allocate(capacity: size) defer { keyList.deallocate() } #if canImport(Darwin) - size = listxattr(path, keyList.baseAddress!, size, 0) + size = listxattr(path, keyList.baseAddress!, size, followSymlinks ? 0 : XATTR_NOFOLLOW) #elseif os(FreeBSD) - size = (followSymlinks ? extattr_list_file : extattr_list_link)(path, EXTATTR_NAMESPACE_USER, nil, 0) + size = (followSymlinks ? extattr_list_file : extattr_list_link)(path, EXTATTR_NAMESPACE_USER, keyList.baseAddress!, size) #else - size = listxattr(path, keyList.baseAddress!, size) + size = followSymlinks ? listxattr(path, keyList.baseAddress!, size) : llistxattr(path, keyList.baseAddress!, size) #endif guard size > 0 else { return nil } @@ -553,7 +553,7 @@ extension _FileManagerImpl { } #endif - if let value = try _extendedAttribute(current, at: path, followSymlinks: false) { + if let value = try _extendedAttribute(current, at: path, followSymlinks: followSymlinks) { extendedAttrs[currentKey] = value } } diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift index d882a67bb..24cb3ec0f 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift @@ -193,15 +193,15 @@ extension _FileManagerImpl { #else var result: Int32 if followSymLinks { - result = lsetxattr(path, key, buffer.baseAddress!, buffer.count, 0) - } else { result = setxattr(path, key, buffer.baseAddress!, buffer.count, 0) + } else { + result = lsetxattr(path, key, buffer.baseAddress!, buffer.count, 0) } #endif #if os(macOS) && FOUNDATION_FRAMEWORK - // if setxaddr failed and its a permission error for a sandbox app trying to set quaratine attribute, ignore it since its not - // permitted, the attribute will be put on the file by the quaratine MAC hook + // if setxattr failed and its a permission error for a sandbox app trying to set quarantine attribute, ignore it since its not + // permitted, the attribute will be put on the file by the quarantine MAC hook if result == -1 && errno == EPERM && _xpc_runtime_is_app_sandboxed() && strcmp(key, "com.apple.quarantine") == 0 { return } diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index 44893c17e..6c592f9c8 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -1046,6 +1046,74 @@ private struct FileManagerTests { } } + #if !os(Windows) && !os(WASI) && !os(OpenBSD) && !canImport(Android) + @Test func extendedAttributesDoNotFollowSymlinksWhenSetting() async throws { + let xattrKey = FileAttributeKey("NSFileExtendedAttributes") + #if canImport(Darwin) + let attrName = "com.swiftfoundation.symlinktest" + let probeName = "com.swiftfoundation.symlinkprobe" + #elseif os(Linux) + // Linux requires the user.* namespace prefix for regular files + let attrName = "user.swiftfoundation.symlinktest" + let probeName = "user.swiftfoundation.symlinkprobe" + #else + let attrName = "swiftfoundation.symlinktest" + let probeName = "swiftfoundation.symlinkprobe" + #endif + let attrValue = Data([0xAA, 0xBB, 0xCC]) + let probeValue = Data([0x11, 0x22, 0x33]) + + try await FilePlayground { + File("target", contents: Data("payload".utf8)) + SymbolicLink("link", destination: "target") + }.test { fileManager in + // First, prove that this environment supports xattrs on regular files and that we + // have permission to set them. If this fails, the symlink behavior isn't meaningful. + do { + try fileManager.setAttributes([xattrKey: [probeName: probeValue]], ofItemAtPath: "target") + } catch let error as CocoaError { + if error.code == .featureUnsupported { return } + guard let posix = error.underlying as? POSIXError else { throw error } + guard posix.code.rawValue == EOPNOTSUPP || posix.code.rawValue == ENOTSUP || posix.code == .EPERM else { throw error } + return + } + + // Attempt to set xattrs on the symlink. + var setSucceeded = false + do { + try fileManager.setAttributes( + [xattrKey: [attrName: attrValue]], ofItemAtPath: "link") + setSucceeded = true + } catch let error as CocoaError { + if error.code == .featureUnsupported { return } + guard let posix = error.underlying as? POSIXError else { throw error } + guard posix.code.rawValue == EOPNOTSUPP || posix.code.rawValue == ENOTSUP || posix.code == .EPERM else { throw error } + // Fall through to verify target wasn't modified + } + + let targetAttrs = try fileManager.attributesOfItem(atPath: "target") + let targetXattrs = targetAttrs[xattrKey] as? [String: Data] + + // The target file must NOT have the xattr - this is the key assertion. + // If setAttributes incorrectly followed the symlink, the xattr would be on the target. + #expect( + targetXattrs?[attrName] == nil, + "setAttributes must not follow symlinks when setting extended attributes") + + if setSucceeded { + // If setting on symlink succeeded, verify it's actually on the symlink + let linkAttrs = try fileManager.attributesOfItem(atPath: "link") + let linkXattrs = try #require( + linkAttrs[xattrKey] as? [String: Data], + "Expected extended attributes on symlink after setAttributes call") + #expect( + linkXattrs[attrName] == attrValue, + "xattr should be applied to the symlink itself") + } + } + } + #endif + #if !canImport(Darwin) || os(macOS) @Test func currentUserHomeDirectory() async throws { let userName = ProcessInfo.processInfo.userName