From cca1aac70822d086c1d3b2b71d974134f7dde9e4 Mon Sep 17 00:00:00 2001 From: YayoRazo Date: Tue, 14 Jul 2026 20:40:59 -0600 Subject: [PATCH] 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. --- Sources/System/Internals/Exports.swift | 9 ++++ Sources/System/Internals/Syscalls.swift | 10 ++-- .../Internals/WindowsSyscallAdapters.swift | 25 ++++----- .../FileOperationsTestWindows.swift | 52 +++++++++++++++++++ 4 files changed, 77 insertions(+), 19 deletions(-) diff --git a/Sources/System/Internals/Exports.swift b/Sources/System/Internals/Exports.swift index 57e5c132..56f7598c 100644 --- a/Sources/System/Internals/Exports.swift +++ b/Sources/System/Internals/Exports.swift @@ -32,7 +32,16 @@ import Android #error("Unsupported Platform") #endif +#if os(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. Use a 64-bit offset type so that +// seeking, positioned I/O, and resizing work on large files, matching the +// 64-bit `off_t` on Linux and Darwin. The Windows syscall adapters route +// this through the 64-bit `_lseeki64` / `OVERLAPPED` / `LARGE_INTEGER` APIs. +internal typealias _COffT = Int64 +#else internal typealias _COffT = off_t +#endif // MARK: syscalls and variables diff --git a/Sources/System/Internals/Syscalls.swift b/Sources/System/Internals/Syscalls.swift index 7deae4d9..740a912c 100644 --- a/Sources/System/Internals/Syscalls.swift +++ b/Sources/System/Internals/Syscalls.swift @@ -72,7 +72,7 @@ internal func system_read( // pread internal func system_pread( - _ fd: Int32, _ buf: UnsafeMutableRawPointer?, _ nbyte: Int, _ offset: off_t + _ fd: Int32, _ buf: UnsafeMutableRawPointer?, _ nbyte: Int, _ offset: _COffT ) -> Int { #if ENABLE_MOCKING if mockingEnabled { return _mockInt(fd, buf, nbyte, offset) } @@ -90,8 +90,8 @@ internal func system_pread( // lseek internal func system_lseek( - _ fd: Int32, _ off: off_t, _ whence: Int32 -) -> off_t { + _ fd: Int32, _ off: _COffT, _ whence: Int32 +) -> _COffT { #if ENABLE_MOCKING if mockingEnabled { return _mockOffT(fd, off, whence) } #endif @@ -110,7 +110,7 @@ internal func system_write( // pwrite internal func system_pwrite( - _ fd: Int32, _ buf: UnsafeRawPointer?, _ nbyte: Int, _ offset: off_t + _ fd: Int32, _ buf: UnsafeRawPointer?, _ nbyte: Int, _ offset: _COffT ) -> Int { #if ENABLE_MOCKING if mockingEnabled { return _mockInt(fd, buf, nbyte, offset) } @@ -165,7 +165,7 @@ internal func system_pipe2(_ fds: UnsafeMutablePointer, _ oflag: Int32) - } #endif -internal func system_ftruncate(_ fd: Int32, _ length: off_t) -> Int32 { +internal func system_ftruncate(_ fd: Int32, _ length: _COffT) -> Int32 { #if ENABLE_MOCKING if mockingEnabled { return _mock(fd, length) } #endif diff --git a/Sources/System/Internals/WindowsSyscallAdapters.swift b/Sources/System/Internals/WindowsSyscallAdapters.swift index 3c6055ec..578a141f 100644 --- a/Sources/System/Internals/WindowsSyscallAdapters.swift +++ b/Sources/System/Internals/WindowsSyscallAdapters.swift @@ -121,13 +121,6 @@ internal func write( Int(_write(fd, buf, numericCast(nbyte))) } -@inline(__always) -internal func lseek( - _ fd: Int32, _ off: off_t, _ whence: Int32 -) -> off_t { - _lseek(fd, off, whence) -} - @inline(__always) internal func dup(_ fd: Int32) -> Int32 { _dup(fd) @@ -144,7 +137,7 @@ internal func dup2(_ fd: Int32, _ fd2: Int32) -> Int32 { @inline(__always) internal func pread( - _ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int, _ offset: off_t + _ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int, _ offset: Int64 ) -> Int { let handle: intptr_t = _get_osfhandle(fd) if handle == /* INVALID_HANDLE_VALUE */ -1 { ucrt._set_errno(EBADF); return -1 } @@ -159,8 +152,10 @@ internal func pread( let hFile: HANDLE = HANDLE(bitPattern: handle)! var ovlOverlapped: OVERLAPPED = OVERLAPPED() - ovlOverlapped.OffsetHigh = DWORD(UInt32(offset >> 32) & 0xffffffff) - ovlOverlapped.Offset = DWORD(UInt32(offset >> 0) & 0xffffffff) + // Split the 64-bit offset into high/low DWORDs. Use truncating conversions: + // `DWORD(UInt32(offset))` would trap for any offset >= 4 GiB. + ovlOverlapped.OffsetHigh = DWORD(truncatingIfNeeded: offset >> 32) + ovlOverlapped.Offset = DWORD(truncatingIfNeeded: offset) var nNumberOfBytesRead: DWORD = 0 if !ReadFile(hFile, buf, DWORD(nbyte), &nNumberOfBytesRead, &ovlOverlapped) { @@ -172,7 +167,7 @@ internal func pread( @inline(__always) internal func pwrite( - _ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int, _ offset: off_t + _ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int, _ offset: Int64 ) -> Int { let handle: intptr_t = _get_osfhandle(fd) if handle == /* INVALID_HANDLE_VALUE */ -1 { ucrt._set_errno(EBADF); return -1 } @@ -187,8 +182,10 @@ internal func pwrite( let hFile: HANDLE = HANDLE(bitPattern: handle)! var ovlOverlapped: OVERLAPPED = OVERLAPPED() - ovlOverlapped.OffsetHigh = DWORD(UInt32(offset >> 32) & 0xffffffff) - ovlOverlapped.Offset = DWORD(UInt32(offset >> 0) & 0xffffffff) + // Split the 64-bit offset into high/low DWORDs. Use truncating conversions: + // `DWORD(UInt32(offset))` would trap for any offset >= 4 GiB. + ovlOverlapped.OffsetHigh = DWORD(truncatingIfNeeded: offset >> 32) + ovlOverlapped.Offset = DWORD(truncatingIfNeeded: offset) var nNumberOfBytesWritten: DWORD = 0 if !WriteFile(hFile, buf, DWORD(nbyte), &nNumberOfBytesWritten, @@ -214,7 +211,7 @@ internal func csystem_posix_pipe2( } @inline(__always) -internal func ftruncate(_ fd: Int32, _ length: off_t) -> Int32 { +internal func ftruncate(_ fd: Int32, _ length: Int64) -> Int32 { let handle: intptr_t = _get_osfhandle(fd) if handle == /* INVALID_HANDLE_VALUE */ -1 { ucrt._set_errno(EBADF); return -1 } diff --git a/Tests/SystemTests/FileOperationsTestWindows.swift b/Tests/SystemTests/FileOperationsTestWindows.swift index 856d74db..5109fbfd 100644 --- a/Tests/SystemTests/FileOperationsTestWindows.swift +++ b/Tests/SystemTests/FileOperationsTestWindows.swift @@ -328,6 +328,58 @@ final class FileOperationsTestWindows: XCTestCase { } } } + + /// Regression test for 64-bit file offsets on Windows. + /// + /// C `off_t` is 32-bit on Windows, so before the offset type was widened, + /// seeking or performing positioned I/O at or beyond 2 GiB trapped at + /// runtime, and the `OVERLAPPED` high dword was always zero so offsets past + /// 4 GiB were unreachable. + func testLargeFileOffsets() throws { + try withTemporaryFilePath(basename: "testLargeFileOffsets") { path in + let fd = try FileDescriptor.open( + path.appending("large.bin"), .readWrite, + options: [.create, .truncate], + permissions: .ownerReadWrite + ) + defer { try? fd.close() } + + // 2 GiB is > Int32.max; 5 GiB is > UInt32.max. Both trapped before the fix. + let twoGiB: Int64 = 1 << 31 + let fiveGiB: Int64 = 5 << 30 + + // Seeking allocates no storage; it exercises `_lseeki64` and must + // round-trip the full 64-bit position rather than trapping or truncating. + XCTAssertEqual(try fd.seek(offset: twoGiB, from: .start), twoGiB) + XCTAssertEqual(try fd.seek(offset: fiveGiB, from: .start), fiveGiB) + + // Positioned read/write beyond 4 GiB exercises both dwords of the + // `OVERLAPPED` offset. Mark the file sparse first so the test does not + // allocate several gigabytes of real storage. + let handle = try XCTUnwrap(HANDLE(bitPattern: _get_osfhandle(fd.rawValue))) + var bytesReturned: DWORD = 0 + let FSCTL_SET_SPARSE: DWORD = 0x000900C4 + try XCTSkipUnless( + DeviceIoControl(handle, FSCTL_SET_SPARSE, nil, 0, nil, 0, + &bytesReturned, nil), + "filesystem does not support sparse files" + ) + + let marker = Array("swift-system".utf8) + let offset = fiveGiB + 123 + let written = try marker.withUnsafeBytes { + try fd.write(toAbsoluteOffset: offset, $0) + } + XCTAssertEqual(written, marker.count) + + var readBack = [UInt8](repeating: 0, count: marker.count) + let read = try readBack.withUnsafeMutableBytes { + try fd.read(fromAbsoluteOffset: offset, into: $0) + } + XCTAssertEqual(read, marker.count) + XCTAssertEqual(readBack, marker) + } + } } #endif // os(Windows)