Skip to content

Fix positioned I/O semantics on Windows and guard read/write sizes - #350

Open
YayoRazo wants to merge 1 commit into
apple:mainfrom
YayoRazo:fix/windows-io-adapter-semantics
Open

Fix positioned I/O semantics on Windows and guard read/write sizes#350
YayoRazo wants to merge 1 commit into
apple:mainfrom
YayoRazo:fix/windows-io-adapter-semantics

Conversation

@YayoRazo

Copy link
Copy Markdown

Summary

Two related robustness fixes in the Windows syscall adapters.

pread / pwrite must not move the file offset

The adapters issue an OVERLAPPED ReadFile/WriteFile at the requested offset, but on a synchronous handle that call also advances the underlying file position. POSIX pread/pwrite must leave the offset untouched, so interleaving positioned and sequential I/O silently read or wrote the wrong bytes (a read after a pread resumed from the wrong place).

Fix: save the current position before the call and restore it in a defer, mirroring what ftruncate already does in the same file.

read / write must guard oversized counts

Unlike pread/pwrite (which already validate nbyte > DWORD.max), the sequential read/write adapters passed the byte count straight to the CRT via numericCast with no bounds check. A count exceeding the unsigned int that _read/_write accept would trap. Add the same nbyte > DWORD.max guard so the error surfaces as EINVAL.

Testing

  • testPositionedIODoesNotMoveFileOffset (cross-platform): parks the file offset, performs a positioned read and write, and asserts the offset is unchanged. Verified it fails without the fix (the offset moves) and passes with it. Because POSIX pread/pwrite never move the offset, this also guards the behavior on Linux/Darwin.
  • testSequentialBufferSizeLimit (Windows): drives an oversized count (DWORD.max + 1) over a small allocation through read and write and asserts EINVAL. Verified it traps without the fix and passes with it.
  • Full swift test suite green on Windows (x86_64-unknown-windows-msvc, Swift 6.3.3); no behavioral change on other platforms.

Two related robustness fixes in the Windows syscall adapters.

pread/pwrite: the adapters issue an OVERLAPPED ReadFile/WriteFile at the
requested offset, but on a synchronous handle that call also advances the
underlying file position. POSIX pread/pwrite must leave the offset untouched,
so interleaving positioned and sequential I/O silently read or wrote the wrong
bytes. Save the current position before the call and restore it in a defer,
mirroring what ftruncate already does. Added a cross-platform regression test.

read/write: unlike pread/pwrite, these passed the byte count straight to the
CRT via numericCast with no bounds check. A count exceeding the unsigned int
that _read/_write accept would trap. Add the same `nbyte > DWORD.max` guard
the positioned variants use so the error surfaces as EINVAL instead, and a
Windows regression test that drives an oversized count through read and write.
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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a way to get a negative value of nbyte to this function from a public entry point, using safe code?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Even allowing for unsafe code, this replaces a trap in numericCast(nbyte) with a thrown error. Is this an improvement? We can note that in this situation the POSIX version effectively throws an Errno(EINVAL).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

According to https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/read?view=msvc-170, the expectation is that _read sets errno to EINVAL for such a case. We should take the nbyte guard, then.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

guard let count = UInt32(exactly: nbyte) else { ... }

@glessard

Copy link
Copy Markdown
Contributor

Could you separate this in two PRs, one for each of the items in the summary above? We much prefer to have one change per commit; it makes the history much more explicable.

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.

2 participants