Skip to content

Commit 15e1703

Browse files
committed
Fix FileManager's extended attribute symlink handling
The if statement appears inverted: setxattr follows simlinks: lsetxattr does not. Additionally, _extendedAttributes was ignoring simlinks entirely. Both of these issues have been addressed.
1 parent 19e3979 commit 15e1703

File tree

3 files changed

+78
-10
lines changed

3 files changed

+78
-10
lines changed

Sources/FoundationEssentials/FileManager/FileManager+Files.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -522,21 +522,21 @@ extension _FileManagerImpl {
522522

523523
private func _extendedAttributes(at path: UnsafePointer<CChar>, followSymlinks: Bool) throws -> [String : Data]? {
524524
#if canImport(Darwin)
525-
var size = listxattr(path, nil, 0, 0)
525+
var size = listxattr(path, nil, 0, followSymlinks ? 0 : XATTR_NOFOLLOW)
526526
#elseif os(FreeBSD)
527527
var size = (followSymlinks ? extattr_list_file : extattr_list_link)(path, EXTATTR_NAMESPACE_USER, nil, 0)
528528
#else
529-
var size = listxattr(path, nil, 0)
529+
var size = followSymlinks ? listxattr(path, nil, 0) : llistxattr(path, nil, 0)
530530
#endif
531531
guard size > 0 else { return nil }
532532
let keyList = UnsafeMutableBufferPointer<CChar>.allocate(capacity: size)
533533
defer { keyList.deallocate() }
534534
#if canImport(Darwin)
535-
size = listxattr(path, keyList.baseAddress!, size, 0)
535+
size = listxattr(path, keyList.baseAddress!, size, followSymlinks ? 0 : XATTR_NOFOLLOW)
536536
#elseif os(FreeBSD)
537-
size = (followSymlinks ? extattr_list_file : extattr_list_link)(path, EXTATTR_NAMESPACE_USER, nil, 0)
537+
size = (followSymlinks ? extattr_list_file : extattr_list_link)(path, EXTATTR_NAMESPACE_USER, keyList.baseAddress!, size)
538538
#else
539-
size = listxattr(path, keyList.baseAddress!, size)
539+
size = followSymlinks ? listxattr(path, keyList.baseAddress!, size) : llistxattr(path, keyList.baseAddress!, size)
540540
#endif
541541
guard size > 0 else { return nil }
542542

@@ -553,7 +553,7 @@ extension _FileManagerImpl {
553553
}
554554
#endif
555555

556-
if let value = try _extendedAttribute(current, at: path, followSymlinks: false) {
556+
if let value = try _extendedAttribute(current, at: path, followSymlinks: followSymlinks) {
557557
extendedAttrs[currentKey] = value
558558
}
559559
}

Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,15 +193,15 @@ extension _FileManagerImpl {
193193
#else
194194
var result: Int32
195195
if followSymLinks {
196-
result = lsetxattr(path, key, buffer.baseAddress!, buffer.count, 0)
197-
} else {
198196
result = setxattr(path, key, buffer.baseAddress!, buffer.count, 0)
197+
} else {
198+
result = lsetxattr(path, key, buffer.baseAddress!, buffer.count, 0)
199199
}
200200
#endif
201201

202202
#if os(macOS) && FOUNDATION_FRAMEWORK
203-
// if setxaddr failed and its a permission error for a sandbox app trying to set quaratine attribute, ignore it since its not
204-
// permitted, the attribute will be put on the file by the quaratine MAC hook
203+
// if setxattr failed and its a permission error for a sandbox app trying to set quarantine attribute, ignore it since its not
204+
// permitted, the attribute will be put on the file by the quarantine MAC hook
205205
if result == -1 && errno == EPERM && _xpc_runtime_is_app_sandboxed() && strcmp(key, "com.apple.quarantine") == 0 {
206206
return
207207
}

Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,74 @@ private struct FileManagerTests {
10461046
}
10471047
}
10481048

1049+
#if !os(Windows) && !os(WASI) && !os(OpenBSD) && !canImport(Android)
1050+
@Test func extendedAttributesDoNotFollowSymlinksWhenSetting() async throws {
1051+
let xattrKey = FileAttributeKey("NSFileExtendedAttributes")
1052+
#if canImport(Darwin)
1053+
let attrName = "com.swiftfoundation.symlinktest"
1054+
let probeName = "com.swiftfoundation.symlinkprobe"
1055+
#elseif os(Linux)
1056+
// Linux requires the user.* namespace prefix for regular files
1057+
let attrName = "user.swiftfoundation.symlinktest"
1058+
let probeName = "user.swiftfoundation.symlinkprobe"
1059+
#else
1060+
let attrName = "swiftfoundation.symlinktest"
1061+
let probeName = "swiftfoundation.symlinkprobe"
1062+
#endif
1063+
let attrValue = Data([0xAA, 0xBB, 0xCC])
1064+
let probeValue = Data([0x11, 0x22, 0x33])
1065+
1066+
try await FilePlayground {
1067+
File("target", contents: Data("payload".utf8))
1068+
SymbolicLink("link", destination: "target")
1069+
}.test { fileManager in
1070+
// First, prove that this environment supports xattrs on regular files and that we
1071+
// have permission to set them. If this fails, the symlink behavior isn't meaningful.
1072+
do {
1073+
try fileManager.setAttributes([xattrKey: [probeName: probeValue]], ofItemAtPath: "target")
1074+
} catch let error as CocoaError {
1075+
if error.code == .featureUnsupported { return }
1076+
guard let posix = error.underlying as? POSIXError else { throw error }
1077+
guard posix.code.rawValue == EOPNOTSUPP || posix.code.rawValue == ENOTSUP else { throw error }
1078+
return
1079+
}
1080+
1081+
// Attempt to set xattrs on the symlink.
1082+
var setSucceeded = false
1083+
do {
1084+
try fileManager.setAttributes(
1085+
[xattrKey: [attrName: attrValue]], ofItemAtPath: "link")
1086+
setSucceeded = true
1087+
} catch let error as CocoaError {
1088+
if error.code == .featureUnsupported { return }
1089+
guard let posix = error.underlying as? POSIXError else { throw error }
1090+
guard posix.code.rawValue == EOPNOTSUPP || posix.code.rawValue == ENOTSUP else { throw error }
1091+
// Fall through to verify target wasn't modified
1092+
}
1093+
1094+
let targetAttrs = try fileManager.attributesOfItem(atPath: "target")
1095+
let targetXattrs = targetAttrs[xattrKey] as? [String: Data]
1096+
1097+
// The target file must NOT have the xattr - this is the key assertion.
1098+
// If setAttributes incorrectly followed the symlink, the xattr would be on the target.
1099+
#expect(
1100+
targetXattrs?[attrName] == nil,
1101+
"setAttributes must not follow symlinks when setting extended attributes")
1102+
1103+
if setSucceeded {
1104+
// If setting on symlink succeeded, verify it's actually on the symlink
1105+
let linkAttrs = try fileManager.attributesOfItem(atPath: "link")
1106+
let linkXattrs = try #require(
1107+
linkAttrs[xattrKey] as? [String: Data],
1108+
"Expected extended attributes on symlink after setAttributes call")
1109+
#expect(
1110+
linkXattrs[attrName] == attrValue,
1111+
"xattr should be applied to the symlink itself")
1112+
}
1113+
}
1114+
}
1115+
#endif
1116+
10491117
#if !canImport(Darwin) || os(macOS)
10501118
@Test func currentUserHomeDirectory() async throws {
10511119
let userName = ProcessInfo.processInfo.userName

0 commit comments

Comments
 (0)