Skip to content

FileHandle.resize(0L) throws after successful SetFilePointer on mingwX64 #1844

Description

@czp3009

Description

Okio 3.18.1 consistently throws from FileHandle.resize(0L) on the Kotlin/Native mingwX64 target when resizing a non-empty file:

okio.IOException: The operation completed successfully.

This is not a transient file-system or concurrency failure. WindowsFileHandle.protectedResize() treats a SetFilePointer() return value of 0 as failure, but Windows documents that a successful move to file position 0 returns 0 because the return value is the low-order DWORD of the new position.

Minimal reproduction

https://github.com/czp3009/okio-mingw-file-handle-resize-reproduction

Core code:

fun main() {
    val fileSystem = FileSystem.SYSTEM
    val path = "test".toPath()

    fileSystem.sink(path).buffer().use { sink ->
        sink.writeUtf8("seed")
    }

    fileSystem.openReadWrite(path, mustExist = true).use { handle ->
        handle.resize(0L)
    }

    check(fileSystem.metadata(path).size == 0L)
}

The Windows native process terminates with an uncaught exception at handle.resize(0L):

Uncaught Kotlin exception: okio.IOException: The operation completed successfully.

The final size assertion is not reached. The exact formatted message can depend on the thread's last-error value and the Windows language, but the erroneous exception is deterministic.

Root cause

The affected code is WindowsFileHandle.protectedResize():

val movePointerResult = SetFilePointer(
    hFile = file,
    lDistanceToMove = size.toInt(),
    lpDistanceToMoveHigh = distanceToMoveHigh.ptr,
    dwMoveMethod = FILE_BEGIN.toUInt(),
)
if (movePointerResult == 0U) {
    throw lastErrorToIOException()
}

According to Microsoft's SetFilePointer documentation:

  • On success, the return value is the low-order DWORD of the new file-pointer position.
  • On failure, the return value is INVALID_SET_FILE_POINTER, not zero.
  • Because INVALID_SET_FILE_POINTER can itself be a valid low-order position, failure with a non-null high-order pointer must be checked as result == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR.

For resize(0L), the call succeeds and returns the new low-order position, which is 0. The movePointerResult == 0U condition therefore enters the error branch after a successful API call. Okio throws before reaching SetEndOfFile.

The observed message is also explained by the Windows API contract:

  • GetLastError should only be read when the preceding function's return value indicates that error information is valid. A successful function does not necessarily clear the thread's last-error value.
  • ERROR_SUCCESS is value 0, with the system message The operation completed successfully.
  • Okio's lastErrorToIOException() formats that value with FormatMessageW and wraps the result in IOException.

The same incorrect condition also affects target positions whose low-order 32 bits are zero. resize(0L) is the smallest stable reproducer.

Suggested minimal fix

The failure condition should follow the documented contract instead of comparing the result with zero:

if (movePointerResult == 0xffffffffU && GetLastError() != 0U) {
    throw lastErrorToIOException()
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions