From 9898e9bcec771b90c395721d2a9cb8e399305f19 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Tue, 11 Aug 2026 22:08:34 +1000 Subject: [PATCH] [common/idd] recovery: reserve IVSHMEM control region Define a fixed, protocol-independent recovery ABI in the final 64 KiB of IVSHMEM. Provide crash-safe multi-client requests and coherent producer status. Exclude the region from LGMP allocation and frame capacity so future protocol mismatches retain a stable control path. --- common/include/common/KVMFRRecovery.h | 261 ++++++++++++++++++ idd/LGIdd/transport/lgmp/CIVSHMEM.h | 21 +- .../transport/lgmp/CLGMPFrameTransport.cpp | 4 +- idd/LGIdd/transport/lgmp/CLGMPHost.cpp | 3 +- idd/LGIdd/transport/lgmp/CLGMPTransport.cpp | 2 +- 5 files changed, 285 insertions(+), 6 deletions(-) create mode 100644 common/include/common/KVMFRRecovery.h diff --git a/common/include/common/KVMFRRecovery.h b/common/include/common/KVMFRRecovery.h new file mode 100644 index 00000000..e0fa1ed0 --- /dev/null +++ b/common/include/common/KVMFRRecovery.h @@ -0,0 +1,261 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_LG_COMMON_KVMFR_RECOVERY_ +#define _H_LG_COMMON_KVMFR_RECOVERY_ + +#pragma once + +#include +#include + +#define KVMFR_R_MAGIC "KVMFRRCV" +#define KVMFR_R_VERSION 1U +#define KVMFR_R_READY 1U + +#define KVMFR_R_REGION_SIZE 65536U +#define KVMFR_R_LINE_SIZE 64U +#define KVMFR_R_HEARTBEAT_MS 250U +#define KVMFR_R_REQ_SLOTS 16U + +enum +{ + KVMFR_R_CAP_DISPLAY = 0x1U +}; + +enum +{ + KVMFR_R_REQ_WRITING = 0x1U, + KVMFR_R_REQ_FIRST = 0x2U +}; + +enum +{ + KVMFR_R_REQ_NONE = 0, + KVMFR_R_REQ_NORMAL = 1, + KVMFR_R_REQ_RECOVERY = 2 +}; + +enum +{ + KVMFR_R_STATE_UNKNOWN = 0, + KVMFR_R_STATE_NORMAL = 1, + KVMFR_R_STATE_SWITCHING = 2, + KVMFR_R_STATE_ACTIVE = 3, + KVMFR_R_STATE_FAILED = 4 +}; + +enum +{ + KVMFR_R_ERR_NONE = 0, + KVMFR_R_ERR_UNSUPPORTED = 1, + KVMFR_R_ERR_HELPER_UNAVAILABLE = 2, + KVMFR_R_ERR_TOPOLOGY_FAILED = 3, + KVMFR_R_ERR_NO_FALLBACK_DISPLAY = 4 +}; + +/* + * The recovery region is outside LGMP and remains stable across LGMP and + * KVMFR protocol changes. Header, information, and status lines are + * producer-owned. Request lines are client-owned except that the producer + * atomically clears a completed slot's serial. Shared fields are plain + * fixed-width values; synchronization is supplied externally rather than + * embedded in the wire layout. + */ +typedef struct KVMFRRHeader +{ + char magic[8]; + uint16_t abiVersion; + uint16_t structSize; + uint32_t capabilities; + uint32_t lgmpVersion; + uint32_t kvmfrVersion; + uint64_t session; + uint8_t uuid[16]; + uint32_t heartbeat; + uint32_t reserved[2]; + uint32_t ready; +} +KVMFRRHeader; + +/* + * Initialized by the IDD before publishing KVMFRRHeader::ready with + * release ordering. Clients acquire ready before reading either producer + * line. The heartbeat is an independently published 32-bit counter. The + * version string is NUL-terminated. + */ +typedef struct KVMFRRInfo +{ + char version[48]; + uint8_t reserved[16]; +} +KVMFRRInfo; + +/* + * Written by clients. Ticket is an even monotonic counter used to allocate a + * unique request serial. Zero is skipped when it wraps. + */ +typedef struct KVMFRRReqHead +{ + uint32_t ticket; + uint8_t reserved[60]; +} +KVMFRRReqHead; + +/* + * Written by clients. A writer claims an empty slot by changing serial from + * zero to its odd ticket, writes the payload, then release-publishes the even + * ticket. The IDD ignores odd slots and atomically clears completed even + * slots. An interrupted writer only consumes its own slot and cannot block or + * corrupt another writer. Session rejects requests from an old producer + * instance. + */ +typedef struct KVMFRRRequest +{ + uint32_t serial; + uint32_t request; + uint64_t session; + uint8_t reserved[48]; +} +KVMFRRRequest; + +/* + * Written only by the IDD. Serial is an independent publication generation: + * the IDD makes it odd before changing the payload, then publishes the next + * nonzero even value with release ordering. Clients use it to take a coherent + * snapshot. AckSerial identifies the request being acknowledged and does not + * provide publication ordering because it remains unchanged as a request + * moves through states. + */ +typedef struct KVMFRRStatus +{ + uint32_t ackSerial; + uint32_t ackRequest; + uint32_t state; + uint32_t error; + uint64_t session; + uint32_t serial; + uint8_t reserved[36]; +} +KVMFRRStatus; + +typedef struct KVMFRR +{ + KVMFRRHeader header; + KVMFRRInfo info; + KVMFRRReqHead req; + KVMFRRRequest requests[KVMFR_R_REQ_SLOTS]; + KVMFRRStatus status; +} +KVMFRR; + +#if defined(__cplusplus) +static_assert(KVMFR_R_REGION_SIZE % KVMFR_R_LINE_SIZE == 0, + "KVMFR recovery region must contain whole cache lines"); +static_assert(sizeof(KVMFRRHeader) == KVMFR_R_LINE_SIZE, + "KVMFR recovery header must occupy one cache line"); +static_assert(offsetof(KVMFRRHeader, lgmpVersion) == 16, + "KVMFR recovery protocol version layout changed"); +static_assert(offsetof(KVMFRRHeader, session) == 24, + "KVMFR recovery session layout changed"); +static_assert(offsetof(KVMFRRHeader, uuid) == 32, + "KVMFR recovery UUID layout changed"); +static_assert(offsetof(KVMFRRHeader, heartbeat) == 48, + "KVMFR recovery heartbeat layout changed"); +static_assert(offsetof(KVMFRRHeader, ready) == 60, + "KVMFR recovery publication layout changed"); +static_assert(sizeof(KVMFRRInfo) == KVMFR_R_LINE_SIZE, + "KVMFR recovery producer information must occupy one cache line"); +static_assert(sizeof(KVMFRRReqHead) == KVMFR_R_LINE_SIZE, + "KVMFR recovery request header must occupy one cache line"); +static_assert(sizeof(KVMFRRRequest) == KVMFR_R_LINE_SIZE, + "KVMFR recovery request must occupy one cache line"); +static_assert(offsetof(KVMFRRRequest, session) == 8, + "KVMFR recovery request session layout changed"); +static_assert(offsetof(KVMFRRRequest, request) == 4, + "KVMFR recovery request publication layout changed"); +static_assert(sizeof(KVMFRRStatus) == KVMFR_R_LINE_SIZE, + "KVMFR recovery status must occupy one cache line"); +static_assert(offsetof(KVMFRRStatus, session) == 16, + "KVMFR recovery status session layout changed"); +static_assert(offsetof(KVMFRRStatus, serial) == 24, + "KVMFR recovery status publication layout changed"); +static_assert(offsetof(KVMFRR, info) == KVMFR_R_LINE_SIZE, + "KVMFR recovery producer information must be cache-line aligned"); +static_assert(offsetof(KVMFRR, req) == KVMFR_R_LINE_SIZE * 2, + "KVMFR recovery request header must be cache-line aligned"); +static_assert(offsetof(KVMFRR, requests) == KVMFR_R_LINE_SIZE * 3, + "KVMFR recovery request must be cache-line aligned"); +static_assert(offsetof(KVMFRR, status) == + KVMFR_R_LINE_SIZE * (3 + KVMFR_R_REQ_SLOTS), + "KVMFR recovery status must be cache-line aligned"); +static_assert(sizeof(KVMFRR) == + KVMFR_R_LINE_SIZE * (4 + KVMFR_R_REQ_SLOTS), + "KVMFR recovery layout changed"); +static_assert(sizeof(KVMFRR) <= KVMFR_R_REGION_SIZE, + "KVMFR recovery data must fit in its reserved region"); +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +_Static_assert(KVMFR_R_REGION_SIZE % KVMFR_R_LINE_SIZE == 0, + "KVMFR recovery region must contain whole cache lines"); +_Static_assert(sizeof(KVMFRRHeader) == KVMFR_R_LINE_SIZE, + "KVMFR recovery header must occupy one cache line"); +_Static_assert(offsetof(KVMFRRHeader, lgmpVersion) == 16, + "KVMFR recovery protocol version layout changed"); +_Static_assert(offsetof(KVMFRRHeader, session) == 24, + "KVMFR recovery session layout changed"); +_Static_assert(offsetof(KVMFRRHeader, uuid) == 32, + "KVMFR recovery UUID layout changed"); +_Static_assert(offsetof(KVMFRRHeader, heartbeat) == 48, + "KVMFR recovery heartbeat layout changed"); +_Static_assert(offsetof(KVMFRRHeader, ready) == 60, + "KVMFR recovery publication layout changed"); +_Static_assert(sizeof(KVMFRRInfo) == KVMFR_R_LINE_SIZE, + "KVMFR recovery producer information must occupy one cache line"); +_Static_assert(sizeof(KVMFRRReqHead) == KVMFR_R_LINE_SIZE, + "KVMFR recovery request header must occupy one cache line"); +_Static_assert(sizeof(KVMFRRRequest) == KVMFR_R_LINE_SIZE, + "KVMFR recovery request must occupy one cache line"); +_Static_assert(offsetof(KVMFRRRequest, session) == 8, + "KVMFR recovery request session layout changed"); +_Static_assert(offsetof(KVMFRRRequest, request) == 4, + "KVMFR recovery request publication layout changed"); +_Static_assert(sizeof(KVMFRRStatus) == KVMFR_R_LINE_SIZE, + "KVMFR recovery status must occupy one cache line"); +_Static_assert(offsetof(KVMFRRStatus, session) == 16, + "KVMFR recovery status session layout changed"); +_Static_assert(offsetof(KVMFRRStatus, serial) == 24, + "KVMFR recovery status publication layout changed"); +_Static_assert(offsetof(KVMFRR, info) == KVMFR_R_LINE_SIZE, + "KVMFR recovery producer information must be cache-line aligned"); +_Static_assert(offsetof(KVMFRR, req) == KVMFR_R_LINE_SIZE * 2, + "KVMFR recovery request header must be cache-line aligned"); +_Static_assert(offsetof(KVMFRR, requests) == KVMFR_R_LINE_SIZE * 3, + "KVMFR recovery request must be cache-line aligned"); +_Static_assert(offsetof(KVMFRR, status) == + KVMFR_R_LINE_SIZE * (3 + KVMFR_R_REQ_SLOTS), + "KVMFR recovery status must be cache-line aligned"); +_Static_assert(sizeof(KVMFRR) == + KVMFR_R_LINE_SIZE * (4 + KVMFR_R_REQ_SLOTS), + "KVMFR recovery layout changed"); +_Static_assert(sizeof(KVMFRR) <= KVMFR_R_REGION_SIZE, + "KVMFR recovery data must fit in its reserved region"); +#endif + +#endif diff --git a/idd/LGIdd/transport/lgmp/CIVSHMEM.h b/idd/LGIdd/transport/lgmp/CIVSHMEM.h index b79bcd79..95a46623 100644 --- a/idd/LGIdd/transport/lgmp/CIVSHMEM.h +++ b/idd/LGIdd/transport/lgmp/CIVSHMEM.h @@ -24,6 +24,8 @@ #include #include +#include "common/KVMFRRecovery.h" + class CIVSHMEM { private: @@ -46,6 +48,21 @@ public: bool Open(); void Close(); - size_t GetSize() const { return m_size; } - void * GetMem () const { return m_mem; } + size_t GetFullSize () const { return m_size; } + size_t GetUsableSize () const + { + if (m_size < KVMFR_R_REGION_SIZE) + return 0; + + return m_size - KVMFR_R_REGION_SIZE; + } + + void * GetMem () const { return m_mem; } + void * GetRecoveryMem () const + { + if (!m_mem || m_size < KVMFR_R_REGION_SIZE) + return nullptr; + + return static_cast(m_mem) + GetUsableSize(); + } }; diff --git a/idd/LGIdd/transport/lgmp/CLGMPFrameTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPFrameTransport.cpp index d014100f..9cf1dbc6 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPFrameTransport.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPFrameTransport.cpp @@ -119,7 +119,7 @@ bool CLGMPFrameTransport::Initialize() void CLGMPFrameTransport::SealMemoryLayout() { m_frameMemoryOffset = - m_ivshmem.GetSize() - m_host.Available(); + m_ivshmem.GetUsableSize() - m_host.Available(); } bool CLGMPFrameTransport::Setup(size_t alignSize) @@ -246,7 +246,7 @@ void CLGMPFrameTransport::DeInit() FrameMemoryLimits CLGMPFrameTransport::GetMemoryLimits() const { FrameMemoryLimits limits; - limits.capacity = m_ivshmem.GetSize(); + limits.capacity = m_ivshmem.GetUsableSize(); limits.frameMemoryOffset = m_frameMemoryOffset; limits.alignment = m_alignSize; limits.maxFrameSize = m_maxFrameSize; diff --git a/idd/LGIdd/transport/lgmp/CLGMPHost.cpp b/idd/LGIdd/transport/lgmp/CLGMPHost.cpp index 16812261..011184b6 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPHost.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPHost.cpp @@ -119,7 +119,8 @@ bool CLGMPHost::Initialize(CIVSHMEM& ivshmem) LGMP_STATUS status; std::string udata = ss.str(); - if ((status = lgmpHostInit(ivshmem.GetMem(), (uint32_t)ivshmem.GetSize(), + if ((status = lgmpHostInit(ivshmem.GetMem(), + (uint32_t)ivshmem.GetUsableSize(), &m_host, (uint32_t)udata.size(), (uint8_t*)&udata[0])) != LGMP_OK) { DEBUG_ERROR("lgmpHostInit Failed: %s", lgmpStatusString(status)); diff --git a/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp index bc28b387..6d3ef3f3 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp @@ -181,5 +181,5 @@ FrameMemoryLimits CLGMPTransport::GetMemoryLimits() const DirectFrameBufferMemory CLGMPTransport::GetDirectMemory() const { - return {m_ivshmem.GetMem(), m_ivshmem.GetSize()}; + return {m_ivshmem.GetMem(), m_ivshmem.GetFullSize()}; }