diff --git a/Sources/System/Internals/WindowsSyscallAdapters.swift b/Sources/System/Internals/WindowsSyscallAdapters.swift index 3c6055ec..e583e41d 100644 --- a/Sources/System/Internals/WindowsSyscallAdapters.swift +++ b/Sources/System/Internals/WindowsSyscallAdapters.swift @@ -111,14 +111,26 @@ internal func lseek( internal func read( _ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int ) -> Int { - Int(_read(fd, buf, numericCast(nbyte))) + // _read takes an unsigned int count; reject sizes that would overflow it so + // numericCast cannot trap (pread/pwrite apply the same guard). + if nbyte > Int(DWORD.max) { + ucrt._set_errno(EINVAL) + return -1 + } + return Int(_read(fd, buf, numericCast(nbyte))) } @inline(__always) internal func write( _ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int ) -> Int { - Int(_write(fd, buf, numericCast(nbyte))) + // _write takes an unsigned int count; reject sizes that would overflow it so + // numericCast cannot trap (pread/pwrite apply the same guard). + if nbyte > Int(DWORD.max) { + ucrt._set_errno(EINVAL) + return -1 + } + return Int(_write(fd, buf, numericCast(nbyte))) } @inline(__always) @@ -158,6 +170,18 @@ internal func pread( // NOTE: this is a non-owning handle, do *not* call CloseHandle on it let hFile: HANDLE = HANDLE(bitPattern: handle)! + // POSIX pread/pwrite leave the file offset unchanged, but issuing an + // OVERLAPPED read/write against a synchronous handle updates it. Save the + // current position and restore it afterwards (as ftruncate does). + var liCurrentOffset = LARGE_INTEGER(QuadPart: 0) + if !SetFilePointerEx(hFile, liCurrentOffset, &liCurrentOffset, FILE_CURRENT) { + ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) + return -1 + } + defer { + _ = SetFilePointerEx(hFile, liCurrentOffset, nil, FILE_BEGIN) + } + var ovlOverlapped: OVERLAPPED = OVERLAPPED() ovlOverlapped.OffsetHigh = DWORD(UInt32(offset >> 32) & 0xffffffff) ovlOverlapped.Offset = DWORD(UInt32(offset >> 0) & 0xffffffff) @@ -186,6 +210,18 @@ internal func pwrite( // NOTE: this is a non-owning handle, do *not* call CloseHandle on it let hFile: HANDLE = HANDLE(bitPattern: handle)! + // POSIX pread/pwrite leave the file offset unchanged, but issuing an + // OVERLAPPED read/write against a synchronous handle updates it. Save the + // current position and restore it afterwards (as ftruncate does). + var liCurrentOffset = LARGE_INTEGER(QuadPart: 0) + if !SetFilePointerEx(hFile, liCurrentOffset, &liCurrentOffset, FILE_CURRENT) { + ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) + return -1 + } + defer { + _ = SetFilePointerEx(hFile, liCurrentOffset, nil, FILE_BEGIN) + } + var ovlOverlapped: OVERLAPPED = OVERLAPPED() ovlOverlapped.OffsetHigh = DWORD(UInt32(offset >> 32) & 0xffffffff) ovlOverlapped.Offset = DWORD(UInt32(offset >> 0) & 0xffffffff) diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index abc4ecdb..be8f48cc 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -153,6 +153,36 @@ final class FileOperationsTest: XCTestCase { } } + func testPositionedIODoesNotMoveFileOffset() throws { + try withTemporaryFilePath(basename: "testPositionedIO") { path in + let fd = try FileDescriptor.open( + path.appending("f.txt"), .readWrite, + options: [.create, .truncate], permissions: .ownerReadWrite) + defer { try? fd.close() } + + try fd.writeAll("0123456789".utf8) + + // Park the file offset at a known position. + XCTAssertEqual(try fd.seek(offset: 3, from: .start), 3) + + // A positioned read must not move the file offset (POSIX pread). + var readBuf = [UInt8](repeating: 0, count: 2) + let n = try readBuf.withUnsafeMutableBytes { + try fd.read(fromAbsoluteOffset: 6, into: $0) + } + XCTAssertEqual(n, 2) + XCTAssertEqual(Array(readBuf), Array("67".utf8)) + XCTAssertEqual(try fd.seek(offset: 0, from: .current), 3) + + // A positioned write must not move the file offset either (POSIX pwrite). + let m = try Array("ab".utf8).withUnsafeBytes { + try fd.write(toAbsoluteOffset: 8, $0) + } + XCTAssertEqual(m, 2) + XCTAssertEqual(try fd.seek(offset: 0, from: .current), 3) + } + } + func testHelpers() { // TODO: Test writeAll, writeAll(toAbsoluteOffset), closeAfter } diff --git a/Tests/SystemTests/FileOperationsTestWindows.swift b/Tests/SystemTests/FileOperationsTestWindows.swift index 856d74db..cc6b8d3c 100644 --- a/Tests/SystemTests/FileOperationsTestWindows.swift +++ b/Tests/SystemTests/FileOperationsTestWindows.swift @@ -297,6 +297,48 @@ final class FileOperationsTestWindows: XCTestCase { } } + /// The sequential read/write adapters must reject a byte count exceeding + /// DWORD.max with EINVAL, matching the positioned pread/pwrite guard. + /// Without the guard, `numericCast(count)` into the CRT's unsigned int would + /// trap. We pass an oversized *count* over a small allocation; the guard + /// rejects it before any bytes are touched. + func testSequentialBufferSizeLimit() throws { + try withTemporaryFilePath(basename: "testSequentialBufferSizeLimit") { path in + let fd = try FileDescriptor.open( + path.appending("test.txt"), + .readWrite, + options: [.create, .truncate], + permissions: .ownerReadWrite + ) + defer { try? fd.close() } + + try fd.writeAll("test data".utf8) + + let buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1024, alignment: 1) + defer { buffer.deallocate() } + + let oversizedCount = Int(DWORD.max) + 1 + let oversizedBuffer = UnsafeMutableRawBufferPointer( + start: buffer.baseAddress, + count: oversizedCount + ) + + do { + _ = try fd.read(into: oversizedBuffer) + XCTFail("Expected EINVAL for buffer size exceeding DWORD.max") + } catch let err as Errno { + XCTAssertEqual(err, .invalidArgument, "Expected EINVAL, got \(err)") + } + + do { + _ = try fd.write(UnsafeRawBufferPointer(oversizedBuffer)) + XCTFail("Expected EINVAL for buffer size exceeding DWORD.max") + } catch let err as Errno { + XCTAssertEqual(err, .invalidArgument, "Expected EINVAL, got \(err)") + } + } + } + func testCloseOnExecOpenOption() throws { try withTemporaryFilePath(basename: "testCloseOnExec") { path in