//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "lldb/Core/Module.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Interpreter/CommandInterpreter.h"
#include "lldb/Interpreter/CommandObjectMultiword.h"
#include "lldb/Interpreter/CommandReturnObject.h"
#include "lldb/Symbol/Type.h"
#include "lldb/Target/DynamicLoader.h"
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/StreamString.h"

#include "Plugins/DynamicLoader/FreeBSD-Kernel/DynamicLoaderFreeBSDKernel.h"
#include "ProcessFreeBSDKernelCore.h"
#include "ThreadFreeBSDKernelCore.h"

using namespace lldb;
using namespace lldb_private;

LLDB_PLUGIN_DEFINE(ProcessFreeBSDKernelCore)

namespace {

#define LLDB_PROPERTIES_processfreebsdkernelcore
#include "ProcessFreeBSDKernelCoreProperties.inc"

enum {
#define LLDB_PROPERTIES_processfreebsdkernelcore
#include "ProcessFreeBSDKernelCorePropertiesEnum.inc"
};

class PluginProperties : public Properties {
public:
  static llvm::StringRef GetSettingName() {
    return ProcessFreeBSDKernelCore::GetPluginNameStatic();
  }

  PluginProperties() : Properties() {
    m_collection_sp = std::make_shared<OptionValueProperties>(GetSettingName());
    m_collection_sp->Initialize(g_processfreebsdkernelcore_properties_def);
  }

  ~PluginProperties() override = default;

  bool GetReadOnly() const {
    const uint32_t idx = ePropertyReadOnly;
    return GetPropertyAtIndexAs<bool>(idx, true);
  }
};

} // namespace

static PluginProperties &GetGlobalPluginProperties() {
  static PluginProperties g_settings;
  return g_settings;
}

class CommandObjectProcessFreeBSDKernelCoreRefreshThreads
    : public CommandObjectParsed {
public:
  CommandObjectProcessFreeBSDKernelCoreRefreshThreads(
      CommandInterpreter &interpreter)
      : CommandObjectParsed(
            interpreter, "process plugin refresh-threads",
            "Refresh the thread list from the FreeBSD kernel core. The thread "
            "list and related data structures may be being read from live "
            "memory (/dev/mem), which may have changed since the last refresh. "
            "This command clears LLDB's thread list and memory cache then "
            "re-reads the kernel's allproc/zombie lists to rebuild the thread "
            "list from scratch.",
            "process plugin refresh-threads",
            eCommandRequiresProcess | eCommandTryTargetAPILock) {}

  ~CommandObjectProcessFreeBSDKernelCoreRefreshThreads() override = default;

protected:
  void DoExecute(Args &command, CommandReturnObject &result) override {
    // TODO: Return early for elf-core based implementation.

    auto process = static_cast<ProcessFreeBSDKernelCore *>(
        m_interpreter.GetExecutionContext().GetProcessPtr());

    // Clear the memory cache so DoUpdateThreadList() will re-read allproc,
    // zombproc, and all thread/proc structures fresh from the core dump instead
    // of getting stale cached values.
    process->m_memory_cache.Clear();

    // Clear both thread lists to guarantee that UpdateThreadListIfNeeded() sees
    // size == 0 and enters the rebuild path regardless of stop-ID state.
    // UpdateThreadListIfNeeded() passes m_thread_list_real as old_thread_list
    // to DoUpdateThreadList(), and DoUpdateThreadList() only rebuilds from
    // scratch when old_thread_list is empty. m_thread_list is the public copy
    // that is sync'd from m_thread_list_real afterwards.
    process->m_thread_list_real.Clear();
    process->m_thread_list.Clear();

    // This calls UpdateThreadListIfNeeded() to rebuild the process thread list.
    const uint32_t num_threads =
        process->GetThreadList().GetSize(/*can_update=*/true);
    result.AppendMessageWithFormatv(
        "Thread list refreshed, {0} thread{1} found.", num_threads,
        num_threads == 1 ? "" : "s");
    result.SetStatus(eReturnStatusSuccessFinishResult);
  }
};

ProcessFreeBSDKernelCore::ProcessFreeBSDKernelCore(lldb::TargetSP target_sp,
                                                   ListenerSP listener_sp,
                                                   const FileSpec &core_file)
    : PostMortemProcess(target_sp, listener_sp, core_file) {}

ProcessFreeBSDKernelCore::~ProcessFreeBSDKernelCore() {
  m_thread_list.Clear();

  // We need to call finalize on the process before destroying ourselves to
  // make sure all of the broadcaster cleanup goes as planned. If we destruct
  // this class, then Process::~Process() might have problems trying to fully
  // destroy the broadcaster.
  Finalize(/*destructing=*/true);
}

lldb::ProcessSP ProcessFreeBSDKernelCore::CreateInstance(
    lldb::TargetSP target_sp, ListenerSP listener_sp,
    const FileSpec *crash_file, bool can_connect) {
  ModuleSP executable = target_sp->GetExecutableModule();
  if (crash_file && !can_connect && executable) {
    char errbuf[_POSIX2_LINE_MAX];
    kvm_t *kvm =
        kvm_open2(executable->GetFileSpec().GetPath().c_str(),
                  crash_file->GetPath().c_str(), O_RDONLY, errbuf, nullptr);
    if (kvm) {
      kvm_close(kvm);
      return std::make_shared<ProcessFreeBSDKernelCore>(target_sp, listener_sp,
                                                        *crash_file);
    }
    LLDB_LOGF(GetLog(LLDBLog::Process), "FreeBSD-Kernel-Core: %s", errbuf);
  }
  return nullptr;
}

void ProcessFreeBSDKernelCore::Initialize() {
  PluginManager::RegisterPlugin(GetPluginNameStatic(),
                                GetPluginDescriptionStatic(), CreateInstance,
                                DebuggerInitialize);
}

void ProcessFreeBSDKernelCore::DebuggerInitialize(Debugger &debugger) {
  if (!PluginManager::GetSettingForProcessPlugin(
          debugger, PluginProperties::GetSettingName())) {
    const bool is_global_setting = true;
    PluginManager::CreateSettingForProcessPlugin(
        debugger, GetGlobalPluginProperties().GetValueProperties(),
        "Properties for the freebsd-kernel process plug-in.",
        is_global_setting);
  }
}

void ProcessFreeBSDKernelCore::Terminate() {
  PluginManager::UnregisterPlugin(ProcessFreeBSDKernelCore::CreateInstance);
}

bool ProcessFreeBSDKernelCore::CanDebug(lldb::TargetSP target_sp,
                                        bool plugin_specified_by_name) {
  return true;
}

CommandObject *ProcessFreeBSDKernelCore::GetPluginCommandObject() {
  if (!m_command_sp) {
    CommandInterpreter &interp =
        GetTarget().GetDebugger().GetCommandInterpreter();
    m_command_sp = std::make_unique<CommandObjectMultiword>(
        interp, "process plugin",
        "Commands for the FreeBSD kernel process plug-in.",
        "process plugin <subcommand> [<subcommand-options>]");
    m_command_sp->LoadSubCommand(
        "refresh-threads",
        CommandObjectSP(
            new CommandObjectProcessFreeBSDKernelCoreRefreshThreads(interp)));
  }
  return m_command_sp.get();
}

Status ProcessFreeBSDKernelCore::DoLoadCore() {
  ModuleSP executable = GetTarget().GetExecutableModule();
  if (!executable)
    return Status::FromErrorString(
        "ProcessFreeBSDKernelCore: no executable module set on target");

  char errbuf[_POSIX2_LINE_MAX];
  m_kvm = kvm_open2(executable->GetFileSpec().GetPath().c_str(),
                    GetCoreFile().GetPath().c_str(), O_RDWR, errbuf, nullptr);

  if (!m_kvm) {
    LLDB_LOGF(GetLog(LLDBLog::Process), "FreeBSD-Kernel-Core: %s", errbuf);
    return Status::FromErrorStringWithFormat(
        "ProcessFreeBSDKernelCore: kvm_open2 failed for core '%s' "
        "with kernel '%s'",
        GetCoreFile().GetPath().c_str(),
        executable->GetFileSpec().GetPath().c_str());
  }

  SetKernelDisplacement();

  return Status();
}

DynamicLoader *ProcessFreeBSDKernelCore::GetDynamicLoader() {
  if (m_dyld_up.get() == nullptr)
    m_dyld_up.reset(DynamicLoader::FindPlugin(
        this, DynamicLoaderFreeBSDKernel::GetPluginNameStatic()));
  return m_dyld_up.get();
}

Status ProcessFreeBSDKernelCore::DoDestroy() {
  if (!m_kvm)
    return Status::FromErrorString("kvm file descriptor is not set.");

  kvm_close(m_kvm);
  return Status();
}

void ProcessFreeBSDKernelCore::RefreshStateAfterStop() {
  if (!m_printed_unread_message) {
    PrintUnreadMessage();
    m_printed_unread_message = true;
  }
}

size_t ProcessFreeBSDKernelCore::DoWriteMemory(lldb::addr_t addr,
                                               const void *buf, size_t size,
                                               Status &error) {
  if (GetGlobalPluginProperties().GetReadOnly()) {
    error = Status::FromErrorString(
        "Memory writes are currently disabled. You can enable them with "
        "`settings set plugin.process.freebsd-kernel-core.read-only false`.");
    return 0;
  }

  ssize_t rd = 0;
  rd = kvm_write(m_kvm, addr, buf, size);
  if (rd < 0 || static_cast<size_t>(rd) != size) {
    error = Status::FromErrorStringWithFormat("Writing memory failed: %s",
                                              GetError());
    return rd > 0 ? rd : 0;
  }
  return rd;
}

bool ProcessFreeBSDKernelCore::DoUpdateThreadList(ThreadList &old_thread_list,
                                                  ThreadList &new_thread_list) {
  if (old_thread_list.GetSize(false) == 0) {
    // Make up the thread the first time this is called so we can set our one
    // and only core thread state up.

    // We cannot construct a thread without a register context as that crashes
    // LLDB but we can construct a process without threads to provide minimal
    // memory reading support.
    switch (GetTarget().GetArchitecture().GetMachine()) {
    case llvm::Triple::arm:
    case llvm::Triple::aarch64:
    case llvm::Triple::ppc64le:
    case llvm::Triple::riscv64:
    case llvm::Triple::x86:
    case llvm::Triple::x86_64:
      break;
    default:
      return false;
    }

    Status error;

    // struct field offsets are written as symbols so that we don't have
    // to figure them out ourselves
    // Process-related offsets:
    int32_t offset_p_list = ReadSignedIntegerFromMemory(
        FindSymbol("proc_off_p_list"), 4, -1, error);
    if (error.Fail())
      return false;

    int32_t offset_p_pid =
        ReadSignedIntegerFromMemory(FindSymbol("proc_off_p_pid"), 4, -1, error);
    if (error.Fail())
      return false;

    int32_t offset_p_threads = ReadSignedIntegerFromMemory(
        FindSymbol("proc_off_p_threads"), 4, -1, error);
    if (error.Fail())
      return false;

    int32_t offset_p_comm = ReadSignedIntegerFromMemory(
        FindSymbol("proc_off_p_comm"), 4, -1, error);
    if (error.Fail())
      return false;

    // Thread-related offsets:
    int32_t offset_td_tid = ReadSignedIntegerFromMemory(
        FindSymbol("thread_off_td_tid"), 4, -1, error);
    if (error.Fail())
      return false;

    int32_t offset_td_plist = ReadSignedIntegerFromMemory(
        FindSymbol("thread_off_td_plist"), 4, -1, error);
    if (error.Fail())
      return false;

    int32_t offset_td_pcb = ReadSignedIntegerFromMemory(
        FindSymbol("thread_off_td_pcb"), 4, -1, error);
    if (error.Fail())
      return false;

    int32_t offset_td_oncpu = ReadSignedIntegerFromMemory(
        FindSymbol("thread_off_td_oncpu"), 4, -1, error);
    if (error.Fail())
      return false;

    int32_t offset_td_name = ReadSignedIntegerFromMemory(
        FindSymbol("thread_off_td_name"), 4, -1, error);
    if (error.Fail())
      return false;

    // Fail if we were not able to read any of the offsets.
    if (offset_p_list == -1 || offset_p_pid == -1 || offset_p_threads == -1 ||
        offset_p_comm == -1 || offset_td_tid == -1 || offset_td_plist == -1 ||
        offset_td_pcb == -1 || offset_td_oncpu == -1 || offset_td_name == -1)
      return false;

    // dumptid contains the thread-id of the crashing thread
    // dumppcb contains its PCB
    int32_t dumptid =
        ReadSignedIntegerFromMemory(FindSymbol("dumptid"), 4, -1, error);
    if (error.Fail())
      return false;

    lldb::addr_t dumppcb = FindSymbol("dumppcb");

    // stoppcbs is an array of PCBs on all CPUs.
    // Each element is of size pcb_size.
    int32_t pcbsize =
        ReadSignedIntegerFromMemory(FindSymbol("pcb_size"), 4, -1, error);
    if (error.Fail())
      return false;

    lldb::addr_t stoppcbs = FindSymbol("stoppcbs");

    // Read stopped_cpus bitmask and mp_maxid for CPU validation.
    lldb::addr_t stopped_cpus = FindSymbol("stopped_cpus");
    uint32_t mp_maxid = 0;

    if (stopped_cpus != LLDB_INVALID_ADDRESS) {
      // https://cgit.freebsd.org/src/tree/sys/kern/subr_smp.c
      mp_maxid =
          ReadSignedIntegerFromMemory(FindSymbol("mp_maxid"), 4, 0, error);
      if (error.Fail())
        stopped_cpus = LLDB_INVALID_ADDRESS;
    }

    uint32_t long_size_bytes = GetAddressByteSize();
    uint32_t long_bit = long_size_bytes * 8;

    if (auto type_system_or_err =
            GetTarget().GetScratchTypeSystemForLanguage(eLanguageTypeC)) {
      CompilerType long_type =
          (*type_system_or_err)->GetBasicTypeFromAST(eBasicTypeLong);
      if (long_type.IsValid())
        if (auto size = long_type.GetByteSize(nullptr))
          long_size_bytes = *size;
      long_bit = long_size_bytes * 8;
    } else
      llvm::consumeError(type_system_or_err.takeError());

    // https://cgit.freebsd.org/src/tree/sys/sys/param.h
    constexpr size_t fbsd_maxcomlen = 19;

    // Iterate through a linked list of all processes then order incrementally
    // by pid. Though new processes are added to the head of this list, process
    // ids may be reused as well. So we cannot rely on it being in a particular
    // order.
    const lldb::addr_t allproc_addr = FindSymbol("allproc");
    if (allproc_addr == LLDB_INVALID_ADDRESS)
      return false;

    std::vector<std::pair<lldb::addr_t, int32_t>> process_addrs;
    for (lldb::addr_t proc = ReadPointerFromMemory(allproc_addr, error);
         error.Success() && proc != 0 && proc != LLDB_INVALID_ADDRESS;
         proc = ReadPointerFromMemory(proc + offset_p_list, error)) {
      int32_t pid =
          ReadSignedIntegerFromMemory(proc + offset_p_pid, 4, -1, error);
      if (error.Fail())
        return false;
      process_addrs.emplace_back(proc, pid);
    }

    if (error.Fail())
      return false;

    std::sort(process_addrs.begin(), process_addrs.end(),
              [](const auto &a, const auto &b) { return a.second < b.second; });

    for (auto [proc, pid] : process_addrs) {
      // process' command-line string
      char comm[fbsd_maxcomlen + 1];
      ReadCStringFromMemory(proc + offset_p_comm, comm, sizeof(comm), error);
      if (error.Fail())
        continue;

      // Iterate through a linked list of all process' threads
      // the initial thread is found in process' p_threads, subsequent
      // elements are linked via td_plist field.
      // If reading memory fails, skip to the next thread.
      for (lldb::addr_t td =
               ReadPointerFromMemory(proc + offset_p_threads, error);
           error.Success() && td != 0;
           td = ReadPointerFromMemory(td + offset_td_plist, error)) {
        int32_t tid =
            ReadSignedIntegerFromMemory(td + offset_td_tid, 4, -1, error);
        if (error.Fail())
          continue;

        lldb::addr_t pcb_addr =
            ReadPointerFromMemory(td + offset_td_pcb, error);
        if (error.Fail())
          continue;

        // whether process was on CPU (-1 if not, otherwise CPU number)
        int32_t oncpu =
            ReadSignedIntegerFromMemory(td + offset_td_oncpu, 4, -2, error);
        if (error.Fail())
          continue;

        // thread name
        char thread_name[fbsd_maxcomlen + 1];
        ReadCStringFromMemory(td + offset_td_name, thread_name,
                              sizeof(thread_name), error);
        if (error.Fail())
          continue;

        // If we failed to read TID, ignore this thread.
        if (tid == -1)
          continue;

        std::string thread_desc = llvm::formatv("(pid {0}) {1}", pid, comm);
        if (*thread_name && strcmp(thread_name, comm)) {
          thread_desc += '/';
          thread_desc += thread_name;
        }

        // Roughly:
        // 1. if the thread crashed, its PCB is going to be at "dumppcb"
        // 2. if the thread was on CPU, its PCB is going to be on the CPU
        // 3. otherwise, its PCB is in the thread struct
        if (tid == dumptid) {
          // NB: dumppcb can be LLDB_INVALID_ADDRESS if reading it failed
          pcb_addr = dumppcb;
          thread_desc += " (crashed)";
        } else if (oncpu != -1) {
          // Verify the CPU is actually in the stopped set before using
          // its stoppcbs entry.
          bool is_stopped = false;
          if (oncpu >= 0 && static_cast<uint32_t>(oncpu) <= mp_maxid &&
              stopped_cpus != LLDB_INVALID_ADDRESS) {
            uint32_t bit = oncpu % long_bit;
            uint32_t word = oncpu / long_bit;
            lldb::addr_t mask_addr = stopped_cpus + word * long_size_bytes;
            uint64_t mask = ReadUnsignedIntegerFromMemory(
                mask_addr, long_size_bytes, 0, error);
            if (error.Success())
              is_stopped = (mask & (1ULL << bit)) != 0;
          }

          // If we managed to read stoppcbs and pcb_size and the cpu is marked
          // as stopped, use them to find the correct PCB.
          if (is_stopped && stoppcbs != LLDB_INVALID_ADDRESS && pcbsize > 0) {
            pcb_addr = stoppcbs + oncpu * pcbsize;
          } else {
            pcb_addr = LLDB_INVALID_ADDRESS;
          }
          thread_desc += llvm::formatv(" (on CPU {0})", oncpu);
        }

        auto thread =
            new ThreadFreeBSDKernelCore(*this, tid, pcb_addr, thread_desc);

        if (tid == dumptid)
          thread->SetIsCrashedThread(true);

        new_thread_list.AddThread(static_cast<ThreadSP>(thread));
      }

      // If reading thread list has failed, return with false.
      if (error.Fail())
        return false;
    }
  } else {
    const uint32_t num_threads = old_thread_list.GetSize(false);
    for (uint32_t i = 0; i < num_threads; ++i)
      new_thread_list.AddThread(old_thread_list.GetThreadAtIndex(i, false));
  }
  return new_thread_list.GetSize(false) > 0;
}

size_t ProcessFreeBSDKernelCore::DoReadMemory(lldb::addr_t addr, void *buf,
                                              size_t size, Status &error) {
  ssize_t rd = 0;
  rd = kvm_read2(m_kvm, addr, buf, size);
  if (rd < 0 || static_cast<size_t>(rd) != size) {
    error = Status::FromErrorStringWithFormat("Reading memory failed: %s",
                                              GetError());
    return rd > 0 ? rd : 0;
  }
  return rd;
}

lldb::addr_t ProcessFreeBSDKernelCore::FindSymbol(const char *name) {
  ModuleSP mod_sp = GetTarget().GetExecutableModule();
  const Symbol *sym = mod_sp->FindFirstSymbolWithNameAndType(ConstString(name));
  return sym ? sym->GetLoadAddress(&GetTarget()) : LLDB_INVALID_ADDRESS;
}

void ProcessFreeBSDKernelCore::SetKernelDisplacement() {
  kssize_t displacement = kvm_kerndisp(m_kvm);

  if (displacement == 0)
    return;

  Target &target = GetTarget();
  lldb::ModuleSP kernel_module_sp = target.GetExecutableModule();
  if (!kernel_module_sp)
    return;

  bool changed = false;
  kernel_module_sp->SetLoadAddress(target,
                                   static_cast<lldb::addr_t>(displacement),
                                   /*value_is_offset=*/true, changed);

  if (changed) {
    ModuleList loaded_module_list;
    loaded_module_list.Append(kernel_module_sp);
    target.ModulesDidLoad(loaded_module_list);
  }
}

void ProcessFreeBSDKernelCore::PrintUnreadMessage() {
  Target &target = GetTarget();
  Debugger &debugger = target.GetDebugger();

  Status error;

  // Find msgbufp symbol (pointer to message buffer)
  lldb::addr_t msgbufp_addr = FindSymbol("msgbufp");
  if (msgbufp_addr == LLDB_INVALID_ADDRESS)
    return;

  // Read the pointer value
  lldb::addr_t msgbufp = ReadPointerFromMemory(msgbufp_addr, error);
  if (error.Fail() || msgbufp == LLDB_INVALID_ADDRESS)
    return;

  // Get the type information for struct msgbuf from DWARF
  TypeQuery query("msgbuf");
  TypeResults results;
  target.GetImages().FindTypes(nullptr, query, results);

  uint64_t offset_msg_ptr = 0;
  uint64_t offset_msg_size = 0;
  uint64_t offset_msg_wseq = 0;
  uint64_t offset_msg_rseq = 0;

  if (results.GetTypeMap().GetSize() > 0) {
    // Found type info - use it to get field offsets
    CompilerType msgbuf_type =
        results.GetTypeMap().GetTypeAtIndex(0)->GetForwardCompilerType();

    uint32_t num_fields = msgbuf_type.GetNumFields();
    int field_found = 0;
    for (uint32_t i = 0; i < num_fields; i++) {
      std::string field_name;
      uint64_t field_offset = 0;

      msgbuf_type.GetFieldAtIndex(i, field_name, &field_offset, nullptr,
                                  nullptr);

      if (field_name == "msg_ptr") {
        offset_msg_ptr = field_offset / 8; // Convert bits to bytes
        field_found++;
      } else if (field_name == "msg_size") {
        offset_msg_size = field_offset / 8;
        field_found++;
      } else if (field_name == "msg_wseq") {
        offset_msg_wseq = field_offset / 8;
        field_found++;
      } else if (field_name == "msg_rseq") {
        offset_msg_rseq = field_offset / 8;
        field_found++;
      }
    }

    if (field_found != 4) {
      LLDB_LOGF(
          GetLog(LLDBLog::Process),
          "FreeBSD-Kernel-Core: Could not find all required fields for msgbuf");
      return;
    }
  } else {
    // Fallback: use hardcoded offsets based on struct layout
    // struct msgbuf layout (from sys/sys/msgbuf.h):
    //   char *msg_ptr;      - offset 0
    //   u_int msg_magic;    - offset ptr_size
    //   u_int msg_size;     - offset ptr_size + 4
    //   u_int msg_wseq;     - offset ptr_size + 8
    //   u_int msg_rseq;     - offset ptr_size + 12
    uint32_t ptr_size = GetAddressByteSize();
    offset_msg_ptr = 0;
    offset_msg_size = ptr_size + 4;
    offset_msg_wseq = ptr_size + 8;
    offset_msg_rseq = ptr_size + 12;
  }

  // Read struct msgbuf fields
  lldb::addr_t bufp = ReadPointerFromMemory(msgbufp + offset_msg_ptr, error);
  if (error.Fail() || bufp == LLDB_INVALID_ADDRESS)
    return;

  uint32_t size =
      ReadUnsignedIntegerFromMemory(msgbufp + offset_msg_size, 4, 0, error);
  if (error.Fail() || size == 0)
    return;

  uint32_t wseq =
      ReadUnsignedIntegerFromMemory(msgbufp + offset_msg_wseq, 4, 0, error);
  if (error.Fail())
    return;

  uint32_t rseq =
      ReadUnsignedIntegerFromMemory(msgbufp + offset_msg_rseq, 4, 0, error);
  if (error.Fail())
    return;

  // Convert sequences to positions
  // MSGBUF_SEQ_TO_POS macro in FreeBSD: ((seq) % (size))
  uint32_t rseq_pos = rseq % size;
  uint32_t wseq_pos = wseq % size;

  if (rseq_pos == wseq_pos)
    return;

  // Print crash info at once using stream
  lldb::StreamSP stream_sp = debugger.GetAsyncOutputStream();
  if (!stream_sp)
    return;

  stream_sp->PutCString("\nUnread portion of the kernel message buffer:\n");

  // Read ring buffer in at most two chunks
  if (rseq_pos < wseq_pos) {
    // No wrap: read from rseq_pos to wseq_pos
    size_t len = wseq_pos - rseq_pos;
    std::string buf(len, '\0');
    size_t bytes_read = ReadMemory(bufp + rseq_pos, &buf[0], len, error);
    if (error.Success() && bytes_read > 0) {
      buf.resize(bytes_read);
      *stream_sp << buf;
    }
  } else {
    // Wrap around: read from rseq_pos to end, then from start to wseq_pos
    size_t len1 = size - rseq_pos;
    std::string buf1(len1, '\0');
    size_t bytes_read1 = ReadMemory(bufp + rseq_pos, &buf1[0], len1, error);
    if (error.Success() && bytes_read1 > 0) {
      buf1.resize(bytes_read1);
      *stream_sp << buf1;
    }

    if (wseq_pos > 0) {
      std::string buf2(wseq_pos, '\0');
      size_t bytes_read2 = ReadMemory(bufp, &buf2[0], wseq_pos, error);
      if (error.Success() && bytes_read2 > 0) {
        buf2.resize(bytes_read2);
        *stream_sp << buf2;
      }
    }
  }

  stream_sp->PutChar('\n');
  stream_sp->Flush();
}

const char *ProcessFreeBSDKernelCore::GetError() { return kvm_geterr(m_kvm); }
