Reading from a pipe and buffer contains only one byte after read when it should contain more

Question:

I am trying to read from a pipe. I successfully opened the pipe and wrote to it, but for some reason, I am having trouble reading from it. There are no errors, but the bytes_read object indicates that 368 bytes were read, while the buffer only contains one byte. I took a look at other examples using the function in Python and tried various changes, but no matter what, I could not fix it. The applicable part of the code is below. The running part of the code calls open_file, then write_file to send a handshake, and then calls read_all_file_contents within a loop.

import ctypes as ct
from ctypes.wintypes import *
from .exceptions import WinapiException


__all__ = (
    "open_file", "read_file", "write_file", "read_all_file_contents",
)


# Winapi constants

GENERIC_READ = -0x80000000
GENERIC_WRITE = 0x40000000
OPEN_EXISTING = 0x3
INVALID_HANDLE_VALUE = -0x1
ERROR_IO_PENDING = 0x3E5


# Winapi structures

class SecurityAttributes(ct.Structure):
    _fields_ = (
        ("nLength", DWORD),
        ("lpSecurityDescriptor", LPVOID),
        ("bInheritHandle", BOOL)
    )


class OverlappedUnionStruct(ct.Structure):
    _fields_ = (
        ("Offset", DWORD),
        ("OffsetHigh", DWORD)
    )


class OverlappedUnion(ct.Union):
    _fields_ = (
        ("Offset", OverlappedUnionStruct),
        ("Pointer", LPVOID)
    )


class Overlapped(ct.Structure):
    _fields_ = (
        ("Internal", PULONG),
        ("InternalHigh", PULONG),
        ("Data", OverlappedUnion),
        ("hEvent", HANDLE)
    )


# Winapi functions

k32 = ct.WinDLL("kernel32")
GetLastError = k32.GetLastError
GetLastError.restype = DWORD
CreateFile = k32.CreateFileA
CreateFile.argtypes = LPCSTR, DWORD, DWORD, SecurityAttributes, DWORD, DWORD, HANDLE
CreateFile.restype = HANDLE
ReadFile = k32.ReadFile
ReadFile.argtypes = HANDLE, LPVOID, DWORD, LPDWORD, ct.POINTER(Overlapped)
ReadFile.restype = BOOL
WriteFile = k32.WriteFile
WriteFile.argtypes = HANDLE, LPCVOID, DWORD, LPDWORD, ct.POINTER(Overlapped)
WriteFile.restype = BOOL
PeekNamedPipe = k32.PeekNamedPipe
PeekNamedPipe.argtypes = HANDLE, LPVOID, DWORD, LPDWORD, LPDWORD, LPDWORD
PeekNamedPipe.restype = BOOL


# Wrapper functions


def _peek_pipe(handle):
    bytes_available = ct.c_ulong()
    if not PeekNamedPipe(handle, None, 0, None, ct.byref(bytes_available), None):
        raise WinapiException(f"PeekNamedPipe failed with error code {GetLastError()}")
    print(f"Bytes available {bytes_available.value}")
    return bytes_available.value


def open_file(file_name, access=GENERIC_READ | GENERIC_WRITE, share_mode=0, security_attribute=None,
              creation_disposition=OPEN_EXISTING, flags=0, template_file=None):
    file_name = ct.c_char_p(file_name.encode())
    if security_attribute is None:
        security_attribute = SecurityAttributes()
    handle = CreateFile(file_name, access, share_mode, security_attribute, creation_disposition,
                        flags, template_file)
    if handle == INVALID_HANDLE_VALUE:
        raise WinapiException(f"CreateFile failed with error code {GetLastError()}")
    return handle


def read_file(handle, size):
    buf = (ct.c_char * size)()
    bytes_read = DWORD(0)
    print(f"reading {size}")
    if ReadFile(handle, ct.byref(buf), size, ct.byref(bytes_read), None):
        print(f"return data read {bytes_read.value}")
        return bytes_read.value, buf.value
    elif error_code := GetLastError() != ERROR_IO_PENDING:
        raise WinapiException(f"ReadFile failed with error code {error_code}")


def read_all_file_contents(handle):
    data = bytes()
    total_bytes_read = 0
    while bytes_available := _peek_pipe(handle):
        print("calling read_file")
        result = read_file(handle, bytes_available)
        if result is None:
            break
        print(result)
        total_bytes_read += result[0]
        data += result[1]
    return total_bytes_read, data


def write_file(handle, data):
    bytes_written = ct.c_ulong()
    print("writing time")
    if WriteFile(handle, data, len(data), ct.byref(bytes_written), Overlapped()):
        print("return bytes read")
        return bytes_written.value
    else:
        raise WinapiException(f"WriteFile failed with error code {GetLastError()}")

The output when running the program is this:

Bytes available 0
...
Bytes available 368
calling read_file
reading 368
return data read 368
(368, b'x01')
Bytes available 0
[Error from the main loop caused by only one byte being returned]

Edit: Here’s a reproducible example if you have discord (it must be open):

from winapi import open_file, read_all_file_contents, write_file
import json
import struct


def payload_to_bytes(opcode, payload):
    payload_bytes = json.dumps(payload).encode()
    header = struct.pack("BL", opcode, len(payload_bytes))
    return header + payload_bytes


handle = open_file("\\?\pipe\discord-ipc-0")

# handshake
write_file(handle, payload_to_bytes(0, {
    "v": 1,
    "client_id": "1032756213445836801"
}))


while True:
    bytes_read, raw = read_all_file_contents(handle)
    if bytes_read == 0:
        continue
    opcode, length, _ = struct.unpack_from("BLh", raw, 0)
    payload = json.loads(raw[8:8 + length].decode())
    print(payload)
    break
Asked By: Sheepposu

||

Answers:

Use .raw instead of .value when reading a byte buffer. .value only reads a null-terminated string. You’re seeing one byte because the next byte is a null. Here’s a fix:

def read_file(handle, size):
    buf = ct.create_string_buffer(size) # also works
    bytes_read = DWORD()                # 0 is the default
    print(f"reading {size}")
    if ReadFile(handle, ct.byref(buf), size, ct.byref(bytes_read), None):
        data = bytes_read.raw[:bytes_read.value] # truncated to bytes read
        print(f"return data read {data}")
        return bytes_read.value, data
    elif error_code := GetLastError() != ERROR_IO_PENDING:
        raise WinapiException(f"ReadFile failed with error code {error_code}")
Answered By: Mark Tolonen
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.