[scudo] Create a non-static getErrorInfo function. (#199770)

Create a getErrorInfo function that operates on the Allocator and
doesn't require passing in all of the extra information.

Add interface function __scudo_get_fault_error_info that calls this new
function.

Add all needed functions to support the new getErrorInfo.

This is being added to replace the static version from Android that
required linking in a copy of libscudo to use. This new version will be
used directly from libc.

GitOrigin-RevId: a0ac7521342567573da5b1e47c94f6199faada39
diff --git a/combined.h b/combined.h
index 72b4b36..b37ff23 100644
--- a/combined.h
+++ b/combined.h
@@ -988,6 +988,186 @@
                : 0;
   }
 
+  void getErrorInfo(uptr FaultAddr, size_t MinDistance, size_t MaxDistance,
+                    scudo_error_report *Reports, size_t &ReportIndex) {
+    auto *RingBuffer = getRingBuffer();
+    if (RingBuffer == nullptr)
+      return;
+
+    // No more room for any more error reports.
+    if (ReportIndex == NumErrorReports)
+      return;
+
+    uptr UntaggedFaultAddr = untagPointer(FaultAddr);
+    BlockInfo Info = Primary.findNearestBlock(UntaggedFaultAddr);
+
+    auto GetBlockInfo = [&](uptr Addr, Chunk::UnpackedHeader *Header,
+                            uptr &ChunkAddr) {
+      uptr ChunkOffset = getChunkOffsetFromBlock(
+          reinterpret_cast<const char *>(loadTagUnaligned(Addr)));
+      ChunkAddr = loadTagUnaligned(Addr + ChunkOffset);
+      uptr HeaderAddr = loadTag(Addr + ChunkOffset - Chunk::getHeaderSize());
+      *Header = *reinterpret_cast<const Chunk::UnpackedHeader *>(HeaderAddr);
+    };
+
+    auto CheckOOB = [&](uptr BlockAddr) {
+      if (BlockAddr < Info.RegionBegin || BlockAddr >= Info.RegionEnd)
+        return false;
+
+      uptr ChunkAddr;
+      Chunk::UnpackedHeader Header;
+      GetBlockInfo(BlockAddr, &Header, ChunkAddr);
+      if (Header.State != Chunk::State::Allocated)
+        return false;
+      const u8 ChunkTag = extractTag(ChunkAddr);
+      if (extractTag(FaultAddr) != ChunkTag) {
+        // If the allocated size is zero, then there isn't a tag on this
+        // pointer, so allow a tag mismatch.
+        if (ChunkTag != 0 || Header.SizeOrUnusedBytes != 0)
+          return false;
+      }
+      auto *Report = &Reports[ReportIndex++];
+      uptr UntaggedChunkAddr = untagPointer(ChunkAddr);
+      const u32 *ChunkData = reinterpret_cast<const u32 *>(UntaggedChunkAddr);
+      Report->error_type = UntaggedFaultAddr < UntaggedChunkAddr
+                               ? BUFFER_UNDERFLOW
+                               : BUFFER_OVERFLOW;
+      Report->allocation_address = UntaggedChunkAddr;
+      Report->allocation_size = Header.SizeOrUnusedBytes;
+      if (RingBuffer->Depot) {
+        const u32 *TracePtr = reinterpret_cast<const u32 *>(loadTagUnaligned(
+            reinterpret_cast<uptr>(&ChunkData[MemTagAllocationTraceIndex])));
+        collectTraceMaybe(RingBuffer->Depot, Report->allocation_trace,
+                          *TracePtr);
+      }
+      const u32 *TidPtr = reinterpret_cast<const u32 *>(loadTagUnaligned(
+          reinterpret_cast<uptr>(&ChunkData[MemTagAllocationTidIndex])));
+      Report->allocation_tid = *TidPtr;
+      return ReportIndex == NumErrorReports;
+    };
+
+    if (MinDistance == 0 && CheckOOB(Info.BlockBegin))
+      return;
+
+    for (size_t I = Max<size_t>(MinDistance, 1); I != MaxDistance; ++I)
+      if (CheckOOB(Info.BlockBegin + I * Info.BlockSize) ||
+          CheckOOB(Info.BlockBegin - I * Info.BlockSize))
+        return;
+  }
+
+  void getRingBufferErrorInfo(uintptr_t FaultAddr, scudo_error_report *Reports,
+                              size_t &ReportIndex) {
+    auto *RingBuffer = getRingBuffer();
+    if (RingBuffer == nullptr)
+      return;
+
+    // No more room for any more error reports.
+    if (ReportIndex == NumErrorReports)
+      return;
+
+    uptr Pos = atomic_load_relaxed(&RingBuffer->Pos);
+    if (Pos == 0)
+      return;
+
+    const uptr RingBufferElements = RingBuffer->RingBufferElements;
+
+    // Pos is a value that is always increasing.
+    uptr LastPos;
+    if (Pos < RingBufferElements) {
+      LastPos = 0;
+    } else {
+      LastPos = Pos - RingBufferElements;
+    }
+    for (uptr I = Pos; I > LastPos; --I) {
+      auto *Entry =
+          getRingBufferEntry(RingBuffer, (I - 1) % RingBufferElements);
+      uptr EntryPtr = atomic_load_relaxed(&Entry->Ptr);
+      if (!EntryPtr)
+        continue;
+
+      uptr UntaggedEntryPtr = untagPointer(EntryPtr);
+      uptr EntrySize = atomic_load_relaxed(&Entry->AllocationSize);
+      u32 AllocationTrace = atomic_load_relaxed(&Entry->AllocationTrace);
+      u32 AllocationTid = atomic_load_relaxed(&Entry->AllocationTid);
+      u32 DeallocationTrace = atomic_load_relaxed(&Entry->DeallocationTrace);
+      u32 DeallocationTid = atomic_load_relaxed(&Entry->DeallocationTid);
+      if (DeallocationTid) {
+        // For UAF we only consider in-bounds fault addresses because
+        // out-of-bounds UAF is rare and attempting to detect it is very likely
+        // to result in false positives.
+        if (FaultAddr < EntryPtr || FaultAddr >= EntryPtr + EntrySize)
+          continue;
+      } else {
+        // Ring buffer OOB is only possible with secondary allocations. In this
+        // case we are guaranteed a guard region of at least a page on either
+        // side of the allocation (guard page on the right, guard page + tagged
+        // region on the left), so ignore any faults outside of that range.
+        if (FaultAddr < EntryPtr - getPageSizeCached() ||
+            FaultAddr >= EntryPtr + EntrySize + getPageSizeCached())
+          continue;
+
+        // For UAF the ring buffer will contain two entries, one for the
+        // allocation and another for the deallocation. Don't report buffer
+        // overflow/underflow using the allocation entry if we have already
+        // collected a report from the deallocation entry.
+        bool Found = false;
+        for (uptr J = 0; J != ReportIndex; ++J) {
+          if (Reports[J].allocation_address == UntaggedEntryPtr) {
+            Found = true;
+            break;
+          }
+        }
+        if (Found)
+          continue;
+      }
+
+      auto *Report = &Reports[ReportIndex++];
+      if (DeallocationTid)
+        Report->error_type = USE_AFTER_FREE;
+      else if (FaultAddr < EntryPtr)
+        Report->error_type = BUFFER_UNDERFLOW;
+      else
+        Report->error_type = BUFFER_OVERFLOW;
+
+      Report->allocation_address = UntaggedEntryPtr;
+      Report->allocation_size = EntrySize;
+      collectTraceMaybe(RingBuffer->Depot, Report->allocation_trace,
+                        AllocationTrace);
+      Report->allocation_tid = AllocationTid;
+      collectTraceMaybe(RingBuffer->Depot, Report->deallocation_trace,
+                        DeallocationTrace);
+      Report->deallocation_tid = DeallocationTid;
+      if (ReportIndex == NumErrorReports) {
+        // No more report entries.
+        return;
+      }
+    }
+  }
+
+  void getErrorInfo(uintptr_t FaultAddr, struct scudo_error_info *ErrorInfo) {
+    const Options Options = Primary.Options.load();
+    if (!useMemoryTagging<AllocatorConfig>(Options))
+      return;
+
+    bool TagExists = extractTag(FaultAddr) != 0;
+    size_t ReportIndex = 0;
+    if (TagExists) {
+      // Check for OOB in the current block and the two surrounding blocks.
+      // Beyond that, UAF is more likely.
+      getErrorInfo(FaultAddr, 0, 2, &ErrorInfo->reports[0], ReportIndex);
+    }
+
+    // Check the ring buffer. For primary allocations this will only find UAF;
+    // for secondary allocations we can find either UAF or OOB.
+    getRingBufferErrorInfo(FaultAddr, &ErrorInfo->reports[0], ReportIndex);
+
+    if (TagExists) {
+      // Check for OOB in the 28 blocks surrounding the 3 we checked earlier.
+      // Beyond that we are likely to hit false positives.
+      getErrorInfo(FaultAddr, 2, 16, &ErrorInfo->reports[0], ReportIndex);
+    }
+  }
+
   static const uptr MaxTraceSize = 64;
 
   static void collectTraceMaybe(const StackDepot *Depot,
diff --git a/include/scudo/interface.h b/include/scudo/interface.h
index 9f2b938..cf80459 100644
--- a/include/scudo/interface.h
+++ b/include/scudo/interface.h
@@ -123,6 +123,9 @@
 const char *__scudo_get_ring_buffer_addr(void);
 size_t __scudo_get_ring_buffer_size(void);
 
+void __scudo_get_fault_error_info(uintptr_t fault_addr,
+                                  struct scudo_error_info *error_info);
+
 #ifndef M_DECAY_TIME
 #define M_DECAY_TIME -100
 #endif
diff --git a/memtag.h b/memtag.h
index 073e72c..653745f 100644
--- a/memtag.h
+++ b/memtag.h
@@ -259,6 +259,11 @@
   return TaggedPtr;
 }
 
+inline uptr loadTagUnaligned(uptr Ptr) {
+  uptr AlignedPtr = Ptr & ~static_cast<uptr>(0xF);
+  return loadTag(AlignedPtr) | (Ptr & 0xF);
+}
+
 #else
 
 inline constexpr bool systemSupportsMemoryTagging() { return false; }
@@ -303,6 +308,11 @@
   UNREACHABLE("memory tagging not supported");
 }
 
+inline uptr loadTagUnaligned(uptr Ptr) {
+  (void)Ptr;
+  UNREACHABLE("memory tagging not supported");
+}
+
 #endif
 
 #pragma GCC diagnostic push
diff --git a/primary32.h b/primary32.h
index 5720c9f..c3705a2 100644
--- a/primary32.h
+++ b/primary32.h
@@ -135,6 +135,9 @@
   static uptr getRegionInfoArraySize() { return 0; }
 
   // Not supported in SizeClassAllocator32.
+  BlockInfo findNearestBlock(UNUSED uptr Ptr) { return {}; }
+
+  // Not supported in SizeClassAllocator32.
   static BlockInfo findNearestBlock(UNUSED const char *RegionInfoData,
                                     UNUSED uptr Ptr) {
     return {};
diff --git a/primary64.h b/primary64.h
index 6ee56d6..2c3ccdf 100644
--- a/primary64.h
+++ b/primary64.h
@@ -93,6 +93,8 @@
   static BlockInfo findNearestBlock(const char *RegionInfoData,
                                     uptr Ptr) NO_THREAD_SAFETY_ANALYSIS;
 
+  BlockInfo findNearestBlock(uptr Ptr);
+
   void init(s32 ReleaseToOsInterval) NO_THREAD_SAFETY_ANALYSIS;
 
   void unmapTestOnly();
@@ -1351,6 +1353,57 @@
 }
 
 template <typename Config>
+BlockInfo SizeClassAllocator64<Config>::findNearestBlock(uptr Ptr)
+    NO_THREAD_SAFETY_ANALYSIS {
+  uptr ClassId;
+  uptr MinDistance = -1UL;
+  for (uptr I = 0; I != NumClasses; ++I) {
+    if (I == SizeClassMap::BatchClassId)
+      continue;
+
+    ScopedLock ML(RegionInfoArray[I].MMLock);
+    uptr Begin = RegionInfoArray[I].RegionBeg;
+    uptr End = Begin + RegionInfoArray[I].MemMapInfo.AllocatedUser;
+    if (Begin > End || End - Begin < SizeClassMap::getSizeByClassId(I))
+      continue;
+    uptr RegionDistance;
+    if (Begin <= Ptr) {
+      if (Ptr < End)
+        RegionDistance = 0;
+      else
+        RegionDistance = Ptr - End;
+    } else {
+      RegionDistance = Begin - Ptr;
+    }
+
+    if (RegionDistance < MinDistance) {
+      MinDistance = RegionDistance;
+      ClassId = I;
+      if (RegionDistance == 0)
+        break;
+    }
+  }
+
+  if (MinDistance > 8192) {
+    return {};
+  }
+
+  ScopedLock ML(RegionInfoArray[ClassId].MMLock);
+  BlockInfo B = {};
+  B.RegionBegin = RegionInfoArray[ClassId].RegionBeg;
+  B.RegionEnd =
+      B.RegionBegin + RegionInfoArray[ClassId].MemMapInfo.AllocatedUser;
+  B.BlockSize = SizeClassMap::getSizeByClassId(ClassId);
+  B.BlockBegin = B.RegionBegin + uptr(sptr(Ptr - B.RegionBegin) /
+                                      sptr(B.BlockSize) * sptr(B.BlockSize));
+  while (B.BlockBegin < B.RegionBegin)
+    B.BlockBegin += B.BlockSize;
+  while (B.RegionEnd < B.BlockBegin + B.BlockSize)
+    B.BlockBegin -= B.BlockSize;
+  return B;
+}
+
+template <typename Config>
 /* static */ BlockInfo SizeClassAllocator64<Config>::findNearestBlock(
     const char *RegionInfoData, uptr Ptr) NO_THREAD_SAFETY_ANALYSIS {
   const RegionInfo *RegionInfoArray =
diff --git a/tests/error_info_test.cpp b/tests/error_info_test.cpp
new file mode 100644
index 0000000..d8d4dc4
--- /dev/null
+++ b/tests/error_info_test.cpp
@@ -0,0 +1,160 @@
+//===-- error_info_test.cpp -------------------------------------*- C++ -*-===//
+//
+// 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 "allocator_config.h"
+#include "combined.h"
+#include "memtag.h"
+#include "tests/scudo_unit_test.h"
+#include <stdlib.h>
+
+namespace scudo {
+
+template <typename Config> struct TestAllocator : Allocator<Config> {
+  TestAllocator() { this->initThreadMaybe(); }
+  ~TestAllocator() { this->unmapTestOnly(); }
+};
+
+template <class TypeParam> struct ScudoErrorInfoTest : public Test {
+  ScudoErrorInfoTest() {
+    setenv("SCUDO_OPTIONS", "allocation_ring_buffer_size=32768", 1);
+    Allocator = std::make_unique<AllocatorT>();
+    Allocator->setTrackAllocationStacks(true);
+  }
+
+  ~ScudoErrorInfoTest() {
+    Allocator->releaseToOS(scudo::ReleaseToOS::Force);
+    unsetenv("SCUDO_OPTIONS");
+  }
+
+  using AllocatorT = TestAllocator<TypeParam>;
+  std::unique_ptr<AllocatorT> Allocator;
+};
+
+struct TestConfig : public scudo::DefaultConfig {
+  static const bool MaySupportMemoryTagging = true;
+};
+
+using ScudoErrorInfoTestTypes = ::testing::Types<TestConfig>;
+TYPED_TEST_SUITE(ScudoErrorInfoTest, ScudoErrorInfoTestTypes);
+
+TYPED_TEST(ScudoErrorInfoTest, RingBufferErrorInfo) {
+  auto *Allocator = this->Allocator.get();
+  if (!scudo::archSupportsMemoryTagging() ||
+      !Allocator->useMemoryTaggingTestOnly()) {
+    GTEST_SKIP() << "MTE not supported or enabled";
+  }
+
+  EXPECT_GT(Allocator->getRingBufferSize(), 0u);
+  EXPECT_NE(nullptr, Allocator->getRingBufferAddress());
+
+  const scudo::uptr Size = 64U;
+  void *P = nullptr;
+  P = Allocator->allocate(Size, Chunk::Origin::Malloc);
+  ASSERT_NE(P, nullptr);
+  Allocator->deallocate(P, Chunk::Origin::Malloc);
+
+  scudo::uptr Ptr = reinterpret_cast<scudo::uptr>(P);
+
+  scudo_error_info ErrorInfo = {};
+  size_t ReportIndex = 0;
+  Allocator->getRingBufferErrorInfo(Ptr, &ErrorInfo.reports[0], ReportIndex);
+
+  EXPECT_EQ(ReportIndex, 1U);
+  EXPECT_EQ(ErrorInfo.reports[0].error_type, USE_AFTER_FREE);
+  EXPECT_EQ(ErrorInfo.reports[0].allocation_address, scudo::untagPointer(Ptr));
+  EXPECT_EQ(ErrorInfo.reports[0].allocation_size, Size);
+
+  // Now verify the ReportIndex is followed.
+  memset(&ErrorInfo, 0, sizeof(ErrorInfo));
+  Allocator->getRingBufferErrorInfo(Ptr, &ErrorInfo.reports[0], ReportIndex);
+  EXPECT_EQ(ReportIndex, 2U);
+  EXPECT_EQ(ErrorInfo.reports[0].error_type, UNKNOWN);
+  EXPECT_EQ(ErrorInfo.reports[1].error_type, USE_AFTER_FREE);
+  EXPECT_EQ(ErrorInfo.reports[1].allocation_address, scudo::untagPointer(Ptr));
+  EXPECT_EQ(ErrorInfo.reports[1].allocation_size, Size);
+
+  // Verify if at max, nothing happens.
+  ReportIndex = 3;
+  memset(&ErrorInfo, 0, sizeof(ErrorInfo));
+  Allocator->getRingBufferErrorInfo(Ptr, &ErrorInfo.reports[0], ReportIndex);
+  EXPECT_EQ(ReportIndex, 3U);
+  EXPECT_EQ(ErrorInfo.reports[0].error_type, UNKNOWN);
+  EXPECT_EQ(ErrorInfo.reports[1].error_type, UNKNOWN);
+  EXPECT_EQ(ErrorInfo.reports[2].error_type, UNKNOWN);
+}
+
+TYPED_TEST(ScudoErrorInfoTest, GetErrorInfoUAF) {
+  auto *Allocator = this->Allocator.get();
+  if (!scudo::archSupportsMemoryTagging() ||
+      !Allocator->useMemoryTaggingTestOnly()) {
+    GTEST_SKIP() << "MTE not supported or enabled";
+  }
+
+  const scudo::uptr Size = 64U;
+  void *P = Allocator->allocate(Size, Chunk::Origin::Malloc);
+  ASSERT_NE(P, nullptr);
+  Allocator->deallocate(P, Chunk::Origin::Malloc);
+
+  scudo::uptr Ptr = reinterpret_cast<scudo::uptr>(P);
+  scudo_error_info ErrorInfo = {};
+  Allocator->getErrorInfo(Ptr, &ErrorInfo);
+
+  EXPECT_EQ(ErrorInfo.reports[0].error_type, USE_AFTER_FREE);
+  EXPECT_EQ(ErrorInfo.reports[0].allocation_address, scudo::untagPointer(Ptr));
+  EXPECT_EQ(ErrorInfo.reports[0].allocation_size, Size);
+}
+
+TYPED_TEST(ScudoErrorInfoTest, GetErrorInfoOverflow) {
+  auto *Allocator = this->Allocator.get();
+  if (!scudo::archSupportsMemoryTagging() ||
+      !Allocator->useMemoryTaggingTestOnly()) {
+    GTEST_SKIP() << "MTE not supported or enabled";
+  }
+
+  const scudo::uptr Size = 64U;
+  void *P = Allocator->allocate(Size, Chunk::Origin::Malloc);
+  ASSERT_NE(P, nullptr);
+
+  scudo::uptr PtrAddr = reinterpret_cast<scudo::uptr>(P);
+  scudo::uptr FaultAddr = PtrAddr + Size;
+  scudo_error_info ErrorInfo = {};
+  Allocator->getErrorInfo(FaultAddr, &ErrorInfo);
+
+  EXPECT_EQ(ErrorInfo.reports[0].error_type, BUFFER_OVERFLOW);
+  EXPECT_EQ(ErrorInfo.reports[0].allocation_address,
+            scudo::untagPointer(PtrAddr));
+  EXPECT_EQ(ErrorInfo.reports[0].allocation_size, Size);
+
+  Allocator->deallocate(P, Chunk::Origin::Malloc);
+}
+
+TYPED_TEST(ScudoErrorInfoTest, GetErrorInfoUnderflow) {
+  auto *Allocator = this->Allocator.get();
+  if (!scudo::archSupportsMemoryTagging() ||
+      !Allocator->useMemoryTaggingTestOnly()) {
+    GTEST_SKIP() << "MTE not supported or enabled";
+  }
+
+  const scudo::uptr Size = 64U;
+  void *P = Allocator->allocate(Size, Chunk::Origin::Malloc);
+  ASSERT_NE(P, nullptr);
+
+  scudo::uptr PtrAddr = reinterpret_cast<scudo::uptr>(P);
+  scudo::uptr FaultAddr = PtrAddr - 1;
+  scudo_error_info ErrorInfo = {};
+  Allocator->getErrorInfo(FaultAddr, &ErrorInfo);
+
+  EXPECT_EQ(ErrorInfo.reports[0].error_type, BUFFER_UNDERFLOW);
+  EXPECT_EQ(ErrorInfo.reports[0].allocation_address,
+            scudo::untagPointer(PtrAddr));
+  EXPECT_EQ(ErrorInfo.reports[0].allocation_size, Size);
+
+  Allocator->deallocate(P, Chunk::Origin::Malloc);
+}
+
+} // namespace scudo
diff --git a/tests/primary_test.cpp b/tests/primary_test.cpp
index 3a087c4..2fbfadf 100644
--- a/tests/primary_test.cpp
+++ b/tests/primary_test.cpp
@@ -237,6 +237,37 @@
   }
 }
 
+SCUDO_TYPED_TEST(ScudoPrimaryTest, FindNearestBlock) {
+  using Primary = TestAllocator<TypeParam, scudo::DefaultSizeClassMap>;
+  std::unique_ptr<Primary> Allocator(new Primary);
+  Allocator->init(/*ReleaseToOsInterval=*/-1);
+  typename Primary::SizeClassAllocatorT SizeClassAllocator;
+  SizeClassAllocator.init(nullptr, Allocator.get());
+
+  const scudo::uptr Size = 64U;
+  if (Primary::canAllocate(Size)) {
+    const scudo::uptr ClassId = Primary::SizeClassMap::getClassIdBySize(Size);
+    void *P = SizeClassAllocator.allocate(ClassId);
+    ASSERT_NE(P, nullptr);
+
+    scudo::uptr Ptr = reinterpret_cast<scudo::uptr>(P);
+    scudo::BlockInfo Info = Allocator->findNearestBlock(Ptr);
+
+    if (Info.BlockSize != 0) {
+      EXPECT_EQ(Info.BlockBegin, Ptr);
+      EXPECT_EQ(Info.BlockSize,
+                Primary::SizeClassMap::getSizeByClassId(ClassId));
+
+      scudo::BlockInfo Info2 = Allocator->findNearestBlock(Ptr + 10);
+      EXPECT_EQ(Info2.BlockBegin, Ptr);
+    }
+
+    SizeClassAllocator.deallocate(ClassId, P);
+  }
+
+  SizeClassAllocator.destroy(nullptr);
+}
+
 struct SmallRegionsConfig {
   static const bool MaySupportMemoryTagging = false;
   template <typename> using TSDRegistryT = void;
diff --git a/wrappers_c.cpp b/wrappers_c.cpp
index 573f0b7..daa0bdf 100644
--- a/wrappers_c.cpp
+++ b/wrappers_c.cpp
@@ -14,9 +14,13 @@
 #include "wrappers_c.h"
 #include "wrappers_c_checks.h"
 
+#include "string_utils.h"
+
 #include <stdint.h>
 #include <stdio.h>
 
+#include <sys/uio.h>
+
 #if defined(SCUDO_PREFIX_NAME)
 #define SCUDO_PREFIX(name) CONCATENATE(SCUDO_PREFIX_NAME, name)
 #define SCUDO_ALLOCATOR_STATIC static
@@ -426,6 +430,13 @@
 INTERFACE void __scudo_print_stats(void) { Allocator.printStats(); }
 
 #if !SCUDO_FUCHSIA
+
+INTERFACE void
+__scudo_get_fault_error_info(uintptr_t fault_address,
+                             struct scudo_error_info *error_info) {
+  Allocator.getErrorInfo(fault_address, error_info);
+}
+
 INTERFACE void __scudo_get_error_info(
     struct scudo_error_info *error_info, uintptr_t fault_addr,
     const char *stack_depot, size_t stack_depot_size, const char *region_info,