Skip to content

FileManager: avoid a TOCTOU issue in computing CWD #1035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 7, 2024

Conversation

compnerd
Copy link
Member

@compnerd compnerd commented Nov 5, 2024

On Windows, we could potentially return a nil for the current working directory in the rare case that the current working directory was changed during the computation:

let dwLength: DWORD = GetCurrentDirectoryW(0, nil)                                // 1
return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) {
    if GetCurrentDirectoryW(dwLength, $0.baseAddress) == dwLength - 1 {           // 2
        return String(decodingCString: $0.baseAddress!, as: UTF16.self)
    }
    return nil                                                                    // 3
}

Consider the case where at step 1, we receive $n$. We then are interrupted, the CWD changed. We then perform step 2, where we receive $m$ (st $m != n$). We would then proceed to point 3, where we return nil. Avoid this TOCTOU issue by repeating this operation to a fixed point.

Because we are guaranteed a current directory on Windows (unless the initial query for the buffer size fails), we will eventually succeed. In order to avoid a DoS attack vector, limit the attempt to quiescence to a fixed number.

Fixes: #1034

@compnerd
Copy link
Member Author

compnerd commented Nov 5, 2024

@swift-ci please test

@compnerd
Copy link
Member Author

compnerd commented Nov 6, 2024

@swift-ci please test

On Windows, we could potentially return a `nil` for the current working
directory in the rare case that the current working directory was
changed during the computation:

```swift
let dwLength: DWORD = GetCurrentDirectoryW(0, nil)                                // 1
return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) {
    if GetCurrentDirectoryW(dwLength, $0.baseAddress) == dwLength - 1 {           // 2
        return String(decodingCString: $0.baseAddress!, as: UTF16.self)
    }
    return nil                                                                    // 3
}
```

Consider the case where at step 1, we receive $n$. We then are
interrupted, the CWD changed. We then perform step 2, where we receive
$m$ (st $m != n$). We would then proceed to point 3, where we return
`nil`. Avoid this TOCTOU issue by repeating this operation to a fixed
point.

Because we are guaranteed a current directory on Windows (unless the
initial query for the buffer size fails), we will eventually succeed. In
order to avoid a DoS attack vector, limit the attempt to quiescence to a
fixed number.
@compnerd
Copy link
Member Author

compnerd commented Nov 6, 2024

@swift-ci please test

Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

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

LGTM but @jmschonfeld should also look at the change.

}
return nil
#else
withUnsafeTemporaryAllocation(of: CChar.self, capacity: FileManager.MAX_PATH_SIZE) { buffer in
guard getcwd(buffer.baseAddress!, FileManager.MAX_PATH_SIZE) != nil else {
Copy link
Contributor

Choose a reason for hiding this comment

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

One quick question - on non-Windows we just allocate a buffer of MAX_PATH_SIZE (1024 bytes IIRC) and assume that the path will fit (returning nil if for some reason it doesn't). Just confirming - I assume there's no equivalent for Windows that we should use either instead of requesting the buffer size or using in the case of the size check failing?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, there is no real equivalent for Windows. The majority of the API surface on Windows is designed to assume that you will query the required sizes. In the case of the file system, the only place there is a small buffer size guarantee is the Win32 API surface and in Foundation we will bypass that layer entirely when working with paths to ensure that long paths work.

@kperryua
Copy link
Contributor

kperryua commented Nov 7, 2024

Is this an established pattern of calling this API? Or: how do other libraries handle this race?

@compnerd
Copy link
Member Author

compnerd commented Nov 7, 2024

@kperryua the established pattern is to call it in a loop until it succeeds.

However, this does open up a possible DoS attack vector where the attacker has access to your address space and is able to inject a detour for GetCurrentWorkingDirectoryW to return the value and change the directory. That is the reason for the bounded retry. It is arguable that if the attacker has the ability to detour the function the DoS attack vector is meaningless as they can execute arbitrary code.

The actual race condition that exists with the loop variant is only possible in a multithreaded scenario where a thread is constantly changing the working directory to a monotonically increasing size (which will obviously be bound as you cannot grow ad infinitum) and you are forced to yield control in between the calls. I'm not sure how likely that is to be hit in practice though.

@compnerd
Copy link
Member Author

compnerd commented Nov 7, 2024

@jmschonfeld oaky to merge?

@compnerd compnerd merged commit a9dc42c into swiftlang:main Nov 7, 2024
3 checks passed
@compnerd compnerd deleted the cwd branch November 7, 2024 18:19
cthielen pushed a commit to cthielen/swift-foundation that referenced this pull request Nov 8, 2024
On Windows, we could potentially return a `nil` for the current working
directory in the rare case that the current working directory was
changed during the computation:

```swift
let dwLength: DWORD = GetCurrentDirectoryW(0, nil)                                // 1
return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) {
    if GetCurrentDirectoryW(dwLength, $0.baseAddress) == dwLength - 1 {           // 2
        return String(decodingCString: $0.baseAddress!, as: UTF16.self)
    }
    return nil                                                                    // 3
}
```

Consider the case where at step 1, we receive $n$. We then are
interrupted, the CWD changed. We then perform step 2, where we receive
$m$ (st $m != n$). We would then proceed to point 3, where we return
`nil`. Avoid this TOCTOU issue by repeating this operation to a fixed
point.

Because we are guaranteed a current directory on Windows (unless the
initial query for the buffer size fails), we will eventually succeed. In
order to avoid a DoS attack vector, limit the attempt to quiescence to a
fixed number.
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.

Race condition in _FileManagerImpl.currentDirectoryPath
4 participants