Skip to content

Commit cca1aac

Browse files
committed
Fix 64-bit file offsets on Windows
On Windows, C `off_t` is `long`, i.e. 32-bit, so it cannot represent file offsets or sizes at or beyond 2 GiB. The offset typealias `_COffT` was unconditionally `off_t`, so `FileDescriptor.seek`, `read`/`write` at an absolute offset, and `resize` funneled a public `Int64` through a 32-bit conversion. Because Swift's numeric conversions are range-checked, any offset >= 2^31 trapped at runtime, and positioned I/O could never target a region past 4 GiB (the `OVERLAPPED` high dword was always zero). Make `_COffT` 64-bit on Windows and thread it through `system_lseek`, `system_pread`, `system_pwrite`, and `system_ftruncate` (unchanged on other platforms, where `_COffT` remains `off_t`). The Windows adapters already had a 64-bit `lseek` overload backed by `_lseeki64`; the now-redundant 32-bit `_lseek` overload is removed so overload resolution selects it. The `OVERLAPPED` offset split now uses truncating conversions, since `DWORD(UInt32(offset))` would itself trap once offsets exceed 4 GiB.
1 parent 03604ff commit cca1aac

4 files changed

Lines changed: 77 additions & 19 deletions

File tree

Sources/System/Internals/Exports.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,16 @@ import Android
3232
#error("Unsupported Platform")
3333
#endif
3434

35+
#if os(Windows)
36+
// On Windows, C `off_t` is `long`, i.e. 32-bit, so it cannot represent file
37+
// offsets or sizes at or beyond 2 GiB. Use a 64-bit offset type so that
38+
// seeking, positioned I/O, and resizing work on large files, matching the
39+
// 64-bit `off_t` on Linux and Darwin. The Windows syscall adapters route
40+
// this through the 64-bit `_lseeki64` / `OVERLAPPED` / `LARGE_INTEGER` APIs.
41+
internal typealias _COffT = Int64
42+
#else
3543
internal typealias _COffT = off_t
44+
#endif
3645

3746
// MARK: syscalls and variables
3847

Sources/System/Internals/Syscalls.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ internal func system_read(
7272

7373
// pread
7474
internal func system_pread(
75-
_ fd: Int32, _ buf: UnsafeMutableRawPointer?, _ nbyte: Int, _ offset: off_t
75+
_ fd: Int32, _ buf: UnsafeMutableRawPointer?, _ nbyte: Int, _ offset: _COffT
7676
) -> Int {
7777
#if ENABLE_MOCKING
7878
if mockingEnabled { return _mockInt(fd, buf, nbyte, offset) }
@@ -90,8 +90,8 @@ internal func system_pread(
9090

9191
// lseek
9292
internal func system_lseek(
93-
_ fd: Int32, _ off: off_t, _ whence: Int32
94-
) -> off_t {
93+
_ fd: Int32, _ off: _COffT, _ whence: Int32
94+
) -> _COffT {
9595
#if ENABLE_MOCKING
9696
if mockingEnabled { return _mockOffT(fd, off, whence) }
9797
#endif
@@ -110,7 +110,7 @@ internal func system_write(
110110

111111
// pwrite
112112
internal func system_pwrite(
113-
_ fd: Int32, _ buf: UnsafeRawPointer?, _ nbyte: Int, _ offset: off_t
113+
_ fd: Int32, _ buf: UnsafeRawPointer?, _ nbyte: Int, _ offset: _COffT
114114
) -> Int {
115115
#if ENABLE_MOCKING
116116
if mockingEnabled { return _mockInt(fd, buf, nbyte, offset) }
@@ -165,7 +165,7 @@ internal func system_pipe2(_ fds: UnsafeMutablePointer<Int32>, _ oflag: Int32) -
165165
}
166166
#endif
167167

168-
internal func system_ftruncate(_ fd: Int32, _ length: off_t) -> Int32 {
168+
internal func system_ftruncate(_ fd: Int32, _ length: _COffT) -> Int32 {
169169
#if ENABLE_MOCKING
170170
if mockingEnabled { return _mock(fd, length) }
171171
#endif

Sources/System/Internals/WindowsSyscallAdapters.swift

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,6 @@ internal func write(
121121
Int(_write(fd, buf, numericCast(nbyte)))
122122
}
123123

124-
@inline(__always)
125-
internal func lseek(
126-
_ fd: Int32, _ off: off_t, _ whence: Int32
127-
) -> off_t {
128-
_lseek(fd, off, whence)
129-
}
130-
131124
@inline(__always)
132125
internal func dup(_ fd: Int32) -> Int32 {
133126
_dup(fd)
@@ -144,7 +137,7 @@ internal func dup2(_ fd: Int32, _ fd2: Int32) -> Int32 {
144137

145138
@inline(__always)
146139
internal func pread(
147-
_ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int, _ offset: off_t
140+
_ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int, _ offset: Int64
148141
) -> Int {
149142
let handle: intptr_t = _get_osfhandle(fd)
150143
if handle == /* INVALID_HANDLE_VALUE */ -1 { ucrt._set_errno(EBADF); return -1 }
@@ -159,8 +152,10 @@ internal func pread(
159152
let hFile: HANDLE = HANDLE(bitPattern: handle)!
160153

161154
var ovlOverlapped: OVERLAPPED = OVERLAPPED()
162-
ovlOverlapped.OffsetHigh = DWORD(UInt32(offset >> 32) & 0xffffffff)
163-
ovlOverlapped.Offset = DWORD(UInt32(offset >> 0) & 0xffffffff)
155+
// Split the 64-bit offset into high/low DWORDs. Use truncating conversions:
156+
// `DWORD(UInt32(offset))` would trap for any offset >= 4 GiB.
157+
ovlOverlapped.OffsetHigh = DWORD(truncatingIfNeeded: offset >> 32)
158+
ovlOverlapped.Offset = DWORD(truncatingIfNeeded: offset)
164159

165160
var nNumberOfBytesRead: DWORD = 0
166161
if !ReadFile(hFile, buf, DWORD(nbyte), &nNumberOfBytesRead, &ovlOverlapped) {
@@ -172,7 +167,7 @@ internal func pread(
172167

173168
@inline(__always)
174169
internal func pwrite(
175-
_ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int, _ offset: off_t
170+
_ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int, _ offset: Int64
176171
) -> Int {
177172
let handle: intptr_t = _get_osfhandle(fd)
178173
if handle == /* INVALID_HANDLE_VALUE */ -1 { ucrt._set_errno(EBADF); return -1 }
@@ -187,8 +182,10 @@ internal func pwrite(
187182
let hFile: HANDLE = HANDLE(bitPattern: handle)!
188183

189184
var ovlOverlapped: OVERLAPPED = OVERLAPPED()
190-
ovlOverlapped.OffsetHigh = DWORD(UInt32(offset >> 32) & 0xffffffff)
191-
ovlOverlapped.Offset = DWORD(UInt32(offset >> 0) & 0xffffffff)
185+
// Split the 64-bit offset into high/low DWORDs. Use truncating conversions:
186+
// `DWORD(UInt32(offset))` would trap for any offset >= 4 GiB.
187+
ovlOverlapped.OffsetHigh = DWORD(truncatingIfNeeded: offset >> 32)
188+
ovlOverlapped.Offset = DWORD(truncatingIfNeeded: offset)
192189

193190
var nNumberOfBytesWritten: DWORD = 0
194191
if !WriteFile(hFile, buf, DWORD(nbyte), &nNumberOfBytesWritten,
@@ -214,7 +211,7 @@ internal func csystem_posix_pipe2(
214211
}
215212

216213
@inline(__always)
217-
internal func ftruncate(_ fd: Int32, _ length: off_t) -> Int32 {
214+
internal func ftruncate(_ fd: Int32, _ length: Int64) -> Int32 {
218215
let handle: intptr_t = _get_osfhandle(fd)
219216
if handle == /* INVALID_HANDLE_VALUE */ -1 { ucrt._set_errno(EBADF); return -1 }
220217

Tests/SystemTests/FileOperationsTestWindows.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,58 @@ final class FileOperationsTestWindows: XCTestCase {
328328
}
329329
}
330330
}
331+
332+
/// Regression test for 64-bit file offsets on Windows.
333+
///
334+
/// C `off_t` is 32-bit on Windows, so before the offset type was widened,
335+
/// seeking or performing positioned I/O at or beyond 2 GiB trapped at
336+
/// runtime, and the `OVERLAPPED` high dword was always zero so offsets past
337+
/// 4 GiB were unreachable.
338+
func testLargeFileOffsets() throws {
339+
try withTemporaryFilePath(basename: "testLargeFileOffsets") { path in
340+
let fd = try FileDescriptor.open(
341+
path.appending("large.bin"), .readWrite,
342+
options: [.create, .truncate],
343+
permissions: .ownerReadWrite
344+
)
345+
defer { try? fd.close() }
346+
347+
// 2 GiB is > Int32.max; 5 GiB is > UInt32.max. Both trapped before the fix.
348+
let twoGiB: Int64 = 1 << 31
349+
let fiveGiB: Int64 = 5 << 30
350+
351+
// Seeking allocates no storage; it exercises `_lseeki64` and must
352+
// round-trip the full 64-bit position rather than trapping or truncating.
353+
XCTAssertEqual(try fd.seek(offset: twoGiB, from: .start), twoGiB)
354+
XCTAssertEqual(try fd.seek(offset: fiveGiB, from: .start), fiveGiB)
355+
356+
// Positioned read/write beyond 4 GiB exercises both dwords of the
357+
// `OVERLAPPED` offset. Mark the file sparse first so the test does not
358+
// allocate several gigabytes of real storage.
359+
let handle = try XCTUnwrap(HANDLE(bitPattern: _get_osfhandle(fd.rawValue)))
360+
var bytesReturned: DWORD = 0
361+
let FSCTL_SET_SPARSE: DWORD = 0x000900C4
362+
try XCTSkipUnless(
363+
DeviceIoControl(handle, FSCTL_SET_SPARSE, nil, 0, nil, 0,
364+
&bytesReturned, nil),
365+
"filesystem does not support sparse files"
366+
)
367+
368+
let marker = Array("swift-system".utf8)
369+
let offset = fiveGiB + 123
370+
let written = try marker.withUnsafeBytes {
371+
try fd.write(toAbsoluteOffset: offset, $0)
372+
}
373+
XCTAssertEqual(written, marker.count)
374+
375+
var readBack = [UInt8](repeating: 0, count: marker.count)
376+
let read = try readBack.withUnsafeMutableBytes {
377+
try fd.read(fromAbsoluteOffset: offset, into: $0)
378+
}
379+
XCTAssertEqual(read, marker.count)
380+
XCTAssertEqual(readBack, marker)
381+
}
382+
}
331383
}
332384

333385
#endif // os(Windows)

0 commit comments

Comments
 (0)