Skip to content

Commit 71eefee

Browse files
authored
Fetching user/group info causes race conditions (#994)
* Avoid racy stdlib functions for fetching user/group info * Refactor naming * Fix build failure
1 parent 8510e20 commit 71eefee

File tree

7 files changed

+83
-71
lines changed

7 files changed

+83
-71
lines changed

Sources/FoundationEssentials/FileManager/FileManager+Files.swift

+4-20
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,6 @@ extension Date {
4141
}
4242

4343
#if !os(Windows)
44-
#if !os(WASI)
45-
private func _nameFor(uid: uid_t) -> String? {
46-
guard let pwd = getpwuid(uid), let name = pwd.pointee.pw_name else {
47-
return nil
48-
}
49-
return String(cString: name)
50-
}
51-
52-
private func _nameFor(gid: gid_t) -> String? {
53-
guard let pwd = getgrgid(gid), let name = pwd.pointee.gr_name else {
54-
return nil
55-
}
56-
return String(cString: name)
57-
}
58-
#endif
59-
6044
extension mode_t {
6145
private var _fileType: FileAttributeType {
6246
switch self & S_IFMT {
@@ -193,10 +177,10 @@ extension stat {
193177
.groupOwnerAccountID : _writeFileAttributePrimitive(st_gid, as: UInt.self)
194178
]
195179
#if !os(WASI)
196-
if let userName = _nameFor(uid: st_uid) {
180+
if let userName = Platform.name(forUID: st_uid) {
197181
result[.ownerAccountName] = userName
198182
}
199-
if let groupName = _nameFor(gid: st_gid) {
183+
if let groupName = Platform.name(forGID: st_gid) {
200184
result[.groupOwnerAccountName] = groupName
201185
}
202186
#endif
@@ -942,8 +926,8 @@ extension _FileManagerImpl {
942926
#else
943927
// Bias toward userID & groupID - try to prevent round trips to getpwnam if possible.
944928
var leaveUnchanged: UInt32 { UInt32(bitPattern: -1) }
945-
let rawUserID = userID.flatMap(uid_t.init) ?? user.flatMap(Self._userAccountNameToNumber) ?? leaveUnchanged
946-
let rawGroupID = groupID.flatMap(gid_t.init) ?? group.flatMap(Self._groupAccountNameToNumber) ?? leaveUnchanged
929+
let rawUserID = userID.flatMap(uid_t.init) ?? user.flatMap(Platform.uid(forName:)) ?? leaveUnchanged
930+
let rawGroupID = groupID.flatMap(gid_t.init) ?? group.flatMap(Platform.gid(forName:)) ?? leaveUnchanged
947931
if chown(fileSystemRepresentation, rawUserID, rawGroupID) != 0 {
948932
throw CocoaError.errorWithFilePath(path, errno: errno, reading: false)
949933
}

Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift

-14
Original file line numberDiff line numberDiff line change
@@ -275,20 +275,6 @@ extension _FileManagerImpl {
275275
}
276276
}
277277
#endif
278-
279-
#if !os(Windows) && !os(WASI)
280-
static func _userAccountNameToNumber(_ name: String) -> uid_t? {
281-
name.withCString { ptr in
282-
getpwnam(ptr)?.pointee.pw_uid
283-
}
284-
}
285-
286-
static func _groupAccountNameToNumber(_ name: String) -> gid_t? {
287-
name.withCString { ptr in
288-
getgrnam(ptr)?.pointee.gr_gid
289-
}
290-
}
291-
#endif
292278
}
293279

294280
extension FileManager {

Sources/FoundationEssentials/FileManager/SearchPaths/FileManager+DarwinSearchPaths.swift

+5-13
Original file line numberDiff line numberDiff line change
@@ -160,20 +160,12 @@ extension String {
160160
guard self == "~" || self.hasPrefix("~/") else {
161161
return self
162162
}
163-
var bufSize = sysconf(_SC_GETPW_R_SIZE_MAX)
164-
if bufSize == -1 {
165-
bufSize = 4096 // A generous guess.
166-
}
167-
return withUnsafeTemporaryAllocation(of: CChar.self, capacity: bufSize) { pwBuff in
168-
var pw: UnsafeMutablePointer<passwd>?
169-
var pwd = passwd()
170-
let euid = geteuid()
171-
let trueUid = euid == 0 ? getuid() : euid
172-
guard getpwuid_r(trueUid, &pwd, pwBuff.baseAddress!, bufSize, &pw) == 0, let pw else {
173-
return self
174-
}
175-
return String(cString: pw.pointee.pw_dir).appendingPathComponent(String(self.dropFirst()))
163+
let euid = geteuid()
164+
let trueUid = euid == 0 ? getuid() : euid
165+
guard let name = Platform.name(forUID: trueUid) else {
166+
return self
176167
}
168+
return name.appendingPathComponent(String(self.dropFirst()))
177169
}
178170
}
179171
#endif // os(macOS) && FOUNDATION_FRAMEWORK

Sources/FoundationEssentials/Platform.swift

+64
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,70 @@ extension Platform {
135135
}
136136
return result
137137
}
138+
139+
#if canImport(Darwin)
140+
typealias Operation<Input, Output> = (Input, UnsafeMutablePointer<Output>?, UnsafeMutablePointer<CChar>?, Int, UnsafeMutablePointer<UnsafeMutablePointer<Output>?>?) -> Int32
141+
#else
142+
typealias Operation<Input, Output> = (Input, UnsafeMutablePointer<Output>, UnsafeMutablePointer<CChar>, Int, UnsafeMutablePointer<UnsafeMutablePointer<Output>?>) -> Int32
143+
#endif
144+
145+
private static func withUserGroupBuffer<Input, Output, R>(_ input: Input, _ output: Output, sizeProperty: Int32, operation: Operation<Input, Output>, block: (Output) throws -> R) rethrows -> R? {
146+
var bufferLen = sysconf(sizeProperty)
147+
if bufferLen == -1 {
148+
bufferLen = 4096 // Generous default size estimate
149+
}
150+
return try withUnsafeTemporaryAllocation(of: CChar.self, capacity: bufferLen) {
151+
var result = output
152+
var ptr: UnsafeMutablePointer<Output>?
153+
let error = operation(input, &result, $0.baseAddress!, bufferLen, &ptr)
154+
guard error == 0 && ptr != nil else {
155+
return nil
156+
}
157+
return try block(result)
158+
}
159+
}
160+
161+
static func uid(forName name: String) -> uid_t? {
162+
withUserGroupBuffer(name, passwd(), sizeProperty: Int32(_SC_GETPW_R_SIZE_MAX), operation: getpwnam_r) {
163+
$0.pw_uid
164+
}
165+
}
166+
167+
static func gid(forName name: String) -> uid_t? {
168+
withUserGroupBuffer(name, group(), sizeProperty: Int32(_SC_GETGR_R_SIZE_MAX), operation: getgrnam_r) {
169+
$0.gr_gid
170+
}
171+
}
172+
173+
static func name(forUID uid: uid_t) -> String? {
174+
withUserGroupBuffer(uid, passwd(), sizeProperty: Int32(_SC_GETPW_R_SIZE_MAX), operation: getpwuid_r) {
175+
String(cString: $0.pw_name)
176+
}
177+
}
178+
179+
static func fullName(forUID uid: uid_t) -> String? {
180+
withUserGroupBuffer(uid, passwd(), sizeProperty: Int32(_SC_GETPW_R_SIZE_MAX), operation: getpwuid_r) {
181+
String(cString: $0.pw_gecos)
182+
}
183+
}
184+
185+
static func name(forGID gid: gid_t) -> String? {
186+
withUserGroupBuffer(gid, group(), sizeProperty: Int32(_SC_GETGR_R_SIZE_MAX), operation: getgrgid_r) {
187+
String(cString: $0.gr_name)
188+
}
189+
}
190+
191+
static func homeDirectory(forUserName userName: String) -> String? {
192+
withUserGroupBuffer(userName, passwd(), sizeProperty: Int32(_SC_GETPW_R_SIZE_MAX), operation: getpwnam_r) {
193+
String(cString: $0.pw_dir)
194+
}
195+
}
196+
197+
static func homeDirectory(forUID uid: uid_t) -> String? {
198+
withUserGroupBuffer(uid, passwd(), sizeProperty: Int32(_SC_GETPW_R_SIZE_MAX), operation: getpwuid_r) {
199+
String(cString: $0.pw_dir)
200+
}
201+
}
138202
}
139203
#endif
140204

Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift

+4-6
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,8 @@ final class _ProcessInfo: Sendable {
178178
#if canImport(Darwin) || canImport(Android) || canImport(Glibc) || canImport(Musl)
179179
// Darwin and Linux
180180
let (euid, _) = Platform.getUGIDs()
181-
if let upwd = getpwuid(euid),
182-
let uname = upwd.pointee.pw_name {
183-
return String(cString: uname)
181+
if let username = Platform.name(forUID: euid) {
182+
return username
184183
} else if let username = self.environment["USER"] {
185184
return username
186185
}
@@ -215,9 +214,8 @@ final class _ProcessInfo: Sendable {
215214
return ""
216215
#elseif canImport(Darwin) || canImport(Android) || canImport(Glibc) || canImport(Musl)
217216
let (euid, _) = Platform.getUGIDs()
218-
if let upwd = getpwuid(euid),
219-
let fullname = upwd.pointee.pw_gecos {
220-
return String(cString: fullname)
217+
if let fullName = Platform.fullName(forUID: euid) {
218+
return fullName
221219
}
222220
return ""
223221
#elseif os(WASI)

Sources/FoundationEssentials/String/String+Path.swift

+6-7
Original file line numberDiff line numberDiff line change
@@ -513,17 +513,16 @@ extension String {
513513

514514
#if !os(WASI) // WASI does not have user concept
515515
// Next, attempt to find the home directory via getpwnam/getpwuid
516-
var pass: UnsafeMutablePointer<passwd>?
517516
if let user {
518-
pass = getpwnam(user)
517+
if let dir = Platform.homeDirectory(forUserName: user) {
518+
return dir.standardizingPath
519+
}
519520
} else {
520521
// We use the real UID instead of the EUID here when the EUID is the root user (i.e. a process has called seteuid(0))
521522
// In this instance, we historically do this to ensure a stable home directory location for processes that call seteuid(0)
522-
pass = getpwuid(Platform.getUGIDs(allowEffectiveRootUID: false).uid)
523-
}
524-
525-
if let dir = pass?.pointee.pw_dir {
526-
return String(cString: dir).standardizingPath
523+
if let dir = Platform.homeDirectory(forUID: Platform.getUGIDs(allowEffectiveRootUID: false).uid) {
524+
return dir.standardizingPath
525+
}
527526
}
528527
#endif // !os(WASI)
529528

Tests/FoundationEssentialsTests/StringTests.swift

-11
Original file line numberDiff line numberDiff line change
@@ -2576,17 +2576,6 @@ final class StringTestsStdlib: XCTestCase {
25762576
expectTrue(availableEncodings.contains("abc".smallestEncoding))
25772577
}
25782578

2579-
func getHomeDir() -> String {
2580-
#if os(macOS)
2581-
return String(cString: getpwuid(getuid()).pointee.pw_dir)
2582-
#elseif canImport(Darwin)
2583-
// getpwuid() returns null in sandboxed apps under iOS simulator.
2584-
return NSHomeDirectory()
2585-
#else
2586-
preconditionFailed("implement")
2587-
#endif
2588-
}
2589-
25902579
func test_addingPercentEncoding() {
25912580
expectEqual(
25922581
"abcd1234",

0 commit comments

Comments
 (0)