Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Sources/FoundationEssentials/FileManager/FileManager+Files.swift
Original file line number Diff line number Diff line change
Expand Up @@ -522,21 +522,21 @@ extension _FileManagerImpl {

private func _extendedAttributes(at path: UnsafePointer<CChar>, 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<CChar>.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 }

Expand All @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
68 changes: 68 additions & 0 deletions Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down