Skip to content

[scudo] Add __scudo_get_info symbol to export stats to a buffer. #130273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

piwicode
Copy link

@piwicode piwicode commented Mar 7, 2025

Also make possible to get the fragmentation stats from the primary allocator.

Currenty, __scudo_print_stats symbol writes the report to the provided printer, which is not convenient for processing the result.

Copy link

github-actions bot commented Mar 7, 2025

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot
Copy link
Member

llvmbot commented Mar 7, 2025

@llvm/pr-subscribers-compiler-rt-sanitizer

Author: None (piwicode)

Changes

Also make possible to get the fragmentation stats from the primary allocator.

Currenty, __scudo_print_stats symbol writes the report to the provided printer, which is not convenient for processing the result.


Full diff: https://github.com/llvm/llvm-project/pull/130273.diff

8 Files Affected:

  • (modified) compiler-rt/lib/scudo/standalone/combined.h (+22-8)
  • (modified) compiler-rt/lib/scudo/standalone/include/scudo/interface.h (+17)
  • (modified) compiler-rt/lib/scudo/standalone/string_utils.cpp (+12)
  • (modified) compiler-rt/lib/scudo/standalone/string_utils.h (+8-2)
  • (modified) compiler-rt/lib/scudo/standalone/tests/combined_test.cpp (+6)
  • (modified) compiler-rt/lib/scudo/standalone/tests/strings_test.cpp (+40)
  • (modified) compiler-rt/lib/scudo/standalone/wrappers_c.cpp (+11)
  • (modified) compiler-rt/lib/scudo/standalone/wrappers_c_bionic.cpp (+10)
diff --git a/compiler-rt/lib/scudo/standalone/combined.h b/compiler-rt/lib/scudo/standalone/combined.h
index 5deb8c97f1c86..7ab17c5d204c0 100644
--- a/compiler-rt/lib/scudo/standalone/combined.h
+++ b/compiler-rt/lib/scudo/standalone/combined.h
@@ -633,14 +633,8 @@ class Allocator {
   // sizing purposes.
   uptr getStats(char *Buffer, uptr Size) {
     ScopedString Str;
-    const uptr Length = getStats(&Str) + 1;
-    if (Length < Size)
-      Size = Length;
-    if (Buffer && Size) {
-      memcpy(Buffer, Str.data(), Size);
-      Buffer[Size - 1] = '\0';
-    }
-    return Length;
+    getStats(&Str);
+    return CopyToBuffer(Str, Buffer, Size);
   }
 
   void printStats() {
@@ -649,6 +643,12 @@ class Allocator {
     Str.output();
   }
 
+  uptr getFragmentationInfo(char *Buffer, uptr Size) {
+    ScopedString Str;
+    Primary.getFragmentationInfo(&Str);
+    return CopyToBuffer(Str, Buffer, Size);
+  }
+
   void printFragmentationInfo() {
     ScopedString Str;
     Primary.getFragmentationInfo(&Str);
@@ -1739,6 +1739,20 @@ class Allocator {
     return (Bytes - sizeof(AllocationRingBuffer)) /
            sizeof(typename AllocationRingBuffer::Entry);
   }
+
+  // Writes the string content to the specified buffer as a null terminated
+  // string, and returns the input string size (including terminal '\0') for
+  // buffer sizing purpose.
+  size_t copyToBuffer(const ScopedString &input, char *output_base,
+                      size_t output_length) {
+    if (!output_base)
+      return 0;
+    // Assume that ScopedString internal vector terminal null exists and is not
+    // reported by length.
+    const size_t written = Min(input.length() + 1, output_length);
+    memcpy(output_base, input.data(), written);
+    return written;
+  }
 };
 
 } // namespace scudo
diff --git a/compiler-rt/lib/scudo/standalone/include/scudo/interface.h b/compiler-rt/lib/scudo/standalone/include/scudo/interface.h
index a2dedea910cc0..6845b40c8b5cc 100644
--- a/compiler-rt/lib/scudo/standalone/include/scudo/interface.h
+++ b/compiler-rt/lib/scudo/standalone/include/scudo/interface.h
@@ -35,6 +35,23 @@ __attribute__((weak)) void __scudo_realloc_deallocate_hook(void *old_ptr);
 
 void __scudo_print_stats(void);
 
+// Reports all allocators configuration and general statistics as a null
+// terminated text string.
+#ifndef M_INFO_TOPIC_STATS
+#define M_INFO_TOPIC_STATS 1
+#endif
+
+// Reports fragmentation statistics of the primary allocation as a null
+// terminated text string.
+#ifndef M_INFO_TOPIC_FRAGMENTATION
+#define M_INFO_TOPIC_FRAGMENTATION 2
+#endif
+
+// Writes allocator statistics to the buffer, truncating to the specified size
+// if necessary. Returns the full report size (before truncation) for buffer
+// sizing purpose, or zero if the topic is not supported.
+size_t __scudo_get_info(uint32_t topic, char *buffer, size_t size);
+
 typedef void (*iterate_callback)(uintptr_t base, size_t size, void *arg);
 
 // Determine the likely cause of a tag check fault or other memory protection
diff --git a/compiler-rt/lib/scudo/standalone/string_utils.cpp b/compiler-rt/lib/scudo/standalone/string_utils.cpp
index e584bd806e579..e60ae991352cf 100644
--- a/compiler-rt/lib/scudo/standalone/string_utils.cpp
+++ b/compiler-rt/lib/scudo/standalone/string_utils.cpp
@@ -238,4 +238,16 @@ void Printf(const char *Format, ...) {
   va_end(Args);
 }
 
+size_t CopyToBuffer(const ScopedString &input, char *output_base,
+                    size_t output_length) {
+  if (output_base && output_length) {
+    // Assume that ScopedString internal vector ends with '\0' and that the
+    // terminal null is not reported by `length`.
+    const size_t written = Min(input.length(), output_length - 1);
+    memcpy(output_base, input.data(), written);
+    output_base[written] = '\0';
+  }
+  return input.length() + 1;
+}
+
 } // namespace scudo
diff --git a/compiler-rt/lib/scudo/standalone/string_utils.h b/compiler-rt/lib/scudo/standalone/string_utils.h
index cf61e150f20e5..ccb3cf516d8fe 100644
--- a/compiler-rt/lib/scudo/standalone/string_utils.h
+++ b/compiler-rt/lib/scudo/standalone/string_utils.h
@@ -19,8 +19,8 @@ namespace scudo {
 class ScopedString {
 public:
   explicit ScopedString() { String.push_back('\0'); }
-  uptr length() { return String.size() - 1; }
-  const char *data() { return String.data(); }
+  uptr length() const { return String.size() - 1; }
+  const char *data() const { return String.data(); }
   void clear() {
     String.clear();
     String.push_back('\0');
@@ -45,6 +45,12 @@ class ScopedString {
 
 void Printf(const char *Format, ...) FORMAT(1, 2);
 
+// Copies the string to the buffer, truncating if necessary.
+// Null-terminates the output if output_length is greater than zero.
+// Returns the original string's size (including null).
+size_t CopyToBuffer(const ScopedString &input, char *output_base,
+                    size_t output_length);
+
 } // namespace scudo
 
 #endif // SCUDO_STRING_UTILS_H_
diff --git a/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp b/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp
index 9d665ef88c402..30f54ae4d0db8 100644
--- a/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp
+++ b/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp
@@ -291,6 +291,12 @@ void ScudoCombinedTest<Config>::BasicTest(scudo::uptr SizeLog) {
 
   Allocator->printStats();
   Allocator->printFragmentationInfo();
+
+  {
+    char buffer[256] = {0};
+    EXPECT_LE(0, Allocator->getStats(buffer, sizeof(buffer)));
+    EXPECT_LE(0, Allocator->getFragmentationInfo(buffer, sizeof(buffer)));
+  }
 }
 
 #define SCUDO_MAKE_BASIC_TEST(SizeLog)                                         \
diff --git a/compiler-rt/lib/scudo/standalone/tests/strings_test.cpp b/compiler-rt/lib/scudo/standalone/tests/strings_test.cpp
index f81e5036e83b0..71968ad308675 100644
--- a/compiler-rt/lib/scudo/standalone/tests/strings_test.cpp
+++ b/compiler-rt/lib/scudo/standalone/tests/strings_test.cpp
@@ -128,6 +128,46 @@ TEST(ScudoStringsTest, Padding) {
   testAgainstLibc<int>("%03d - %03d", -12, -1234);
 }
 
+TEST(ScudoStringsTest, CopyToBuffer) {
+  { // Call with null buffer.
+    scudo::ScopedString Str;
+    Str.append("abc");
+    EXPECT_EQ(4U, scudo::CopyToBuffer(Str, nullptr, 0));
+  }
+
+  { // Empty string.
+    scudo::ScopedString Str;
+    char buf[256] = {'0', '1'};
+    EXPECT_EQ(1U, scudo::CopyToBuffer(Str, buf, sizeof(buf)));
+    EXPECT_STREQ("", buf);
+    EXPECT_EQ(0, buf[0]); // Rest of the buffer remains unchanged.
+  }
+
+  { // Empty string in empty buffer.
+    scudo::ScopedString Str;
+    char buf[256] = {'0', '1'};
+    EXPECT_EQ(1U, scudo::CopyToBuffer(Str, buf, 0));
+    EXPECT_EQ('0', buf[0]); // Nothing changed because provided size is 0.
+  }
+
+  { // Some text fittinh in the buffer.
+    scudo::ScopedString Str;
+    Str.append("abc");
+    char buf[256] = {'0', '1', '2', '3', '4', '5'};
+    EXPECT_EQ(4U, scudo::CopyToBuffer(
+                      Str, buf, sizeof(buf))); // Size includes terminal null.
+    EXPECT_STREQ("abc", buf);
+  }
+
+  { // Some text with overflows.
+    scudo::ScopedString Str;
+    Str.append("abc");
+    char buf[256] = {'0', '1', '2', '3', '4', '5'};
+    EXPECT_EQ(4U, scudo::CopyToBuffer(Str, buf, 3));
+    EXPECT_STREQ("ab", buf);
+  }
+}
+
 #if defined(__linux__)
 
 #include <sys/resource.h>
diff --git a/compiler-rt/lib/scudo/standalone/wrappers_c.cpp b/compiler-rt/lib/scudo/standalone/wrappers_c.cpp
index 60014a0f66bf5..725e3f3e25996 100644
--- a/compiler-rt/lib/scudo/standalone/wrappers_c.cpp
+++ b/compiler-rt/lib/scudo/standalone/wrappers_c.cpp
@@ -37,4 +37,15 @@ scudo::Allocator<scudo::Config, SCUDO_PREFIX(malloc_postinit)> SCUDO_ALLOCATOR;
 
 extern "C" INTERFACE void __scudo_print_stats(void) { Allocator.printStats(); }
 
+extern "C" INTERFACE size_t __scudo_get_info(uint32_t topic, char *buffer,
+                                             size_t size) {
+  switch (topic) {
+  case M_INFO_TOPIC_STATS:
+    return Allocator.getStats(buffer, size);
+  case M_INFO_TOPIC_FRAGMENTATION:
+    return Allocator.getFragmentationInfo(buffer, size);
+  }
+  return 0;
+}
+
 #endif // !SCUDO_ANDROID || !_BIONIC
diff --git a/compiler-rt/lib/scudo/standalone/wrappers_c_bionic.cpp b/compiler-rt/lib/scudo/standalone/wrappers_c_bionic.cpp
index e9d8c1e8d3db2..f649059b9c878 100644
--- a/compiler-rt/lib/scudo/standalone/wrappers_c_bionic.cpp
+++ b/compiler-rt/lib/scudo/standalone/wrappers_c_bionic.cpp
@@ -38,6 +38,16 @@ static scudo::Allocator<scudo::Config, SCUDO_PREFIX(malloc_postinit)>
 // TODO(kostyak): support both allocators.
 INTERFACE void __scudo_print_stats(void) { Allocator.printStats(); }
 
+INTERFACE size_t __scudo_get_info(uint32_t topic, char *buffer, size_t size) {
+  switch (topic) {
+  case M_INFO_TOPIC_STATS:
+    return Allocator.getStats(buffer, size);
+  case M_INFO_TOPIC_FRAGMENTATION:
+    return Allocator.getFragmentationInfo(buffer, size);
+  }
+  return 0;
+}
+
 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,

@piwicode piwicode force-pushed the main branch 4 times, most recently from 58148b5 to 8d5d2de Compare March 11, 2025 09:13
@fabio-d fabio-d requested a review from ChiaHungDuan March 11, 2025 12:04
// Writes allocator statistics to the buffer, truncating to the specified size
// if necessary. Returns the full report size (before truncation) for buffer
// sizing purpose, or zero if the topic is not supported.
size_t __scudo_get_info(uint32_t topic, void *buffer, size_t size);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All parameters are capitalized.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like parameters use snake_case in the interface.h. Could you please confirm that we want to Capitalize?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, we have so many different cases. We should probably change this in the future, but your interface is fine.

Copy link
Author

@piwicode piwicode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Please take another look.

// Writes allocator statistics to the buffer, truncating to the specified size
// if necessary. Returns the full report size (before truncation) for buffer
// sizing purpose, or zero if the topic is not supported.
size_t __scudo_get_info(uint32_t topic, void *buffer, size_t size);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like parameters use snake_case in the interface.h. Could you please confirm that we want to Capitalize?

// Writes allocator statistics to the buffer, truncating to the specified size
// if necessary. Returns the full report size (before truncation) for buffer
// sizing purpose, or zero if the topic is not supported.
size_t __scudo_get_info(uint32_t topic, void *buffer, size_t size);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, we have so many different cases. We should probably change this in the future, but your interface is fine.

// Writes allocator statistics to the buffer, truncating to the specified size
// if necessary. Returns the full report size (before truncation) for buffer
// sizing purpose, or zero if the topic is not supported.
size_t __scudo_get_info(uint32_t topic, void *buffer, size_t size);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I had a comment about this, but I may have missed your reply.

Is there a reason to have this as one function and not two different functions that do different things? For example, are you planning to add more info types in the future.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I want to allow for future topic additions without altering the binary interface. This approach enables us to experiment with new topics and validate their usefulness before formally documenting them. It also provides a graceful way to deprecate topics without breaking existing callers or cluttering the symbol table.

Copy link
Contributor

@cferris1000 cferris1000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@@ -229,6 +229,15 @@ void ScopedString::append(const char *Format, ...) {
va_end(Args);
}

size_t ScopedString::copyToBuffer(char *OutputBase, size_t OutputLength) {
if (OutputBase && OutputLength) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case we may need the null check here? If possible, I tend to make it always non-null. (a DCHECK if needed)

Copy link
Author

@piwicode piwicode Jun 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, I'm reusing the existing APIs style.
In combined.h, getStats uses that protocol, where the caller can provide a buffer, and if it is too small, the value gets truncated, and the caller knows it from the returned size being larger that the provided buffer. Calling with null returns the required buffer size.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I think that needs to be fixed as well. Let's do the non-null check and returning number of bytes copied here first

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I added a DCHECK to prevent call with null.
I left the original getstat code path unchanged, that is to say it accepts null buffer, and I will update that codepath in a separate change.

Comment on lines +339 to +340
EXPECT_LE(0, Allocator->getStats(buffer, sizeof(buffer)));
EXPECT_LE(0, Allocator->getFragmentationInfo(buffer, sizeof(buffer)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change these to EXPECT_GT(Allocator->..., 0);

Same as the uses in the tests

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

memcpy(OutputBase, data(), Written);
OutputBase[Written] = '\0';
}
return length() + 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should return the length of data copied?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I tried to conform to existing APIs, they are a bit surprising, but it makes sense.

In combined.h, getStats uses that protocol, where the caller can provide a buffer, and if it is too small, the value gets truncated, and the caller knows it from the returned size. I reuse the same principle here.

Comment on lines 131 to 135
TEST(ScudoStringsTest, CopyIntoNullBuffer) {
scudo::ScopedString Str;
Str.append("abc");
EXPECT_EQ(4U, Str.copyToBuffer(nullptr, 0));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we agree the suggestion mentioned above, we may not need this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I removed the test.

TEST(ScudoStringsTest, CopyIntoLargeEnoughBuffer) {
scudo::ScopedString Str;
Str.append("abc");
char buf[256] = {'0', '1', '2', '3', '4', '5'};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that we don't need to init the buf with string nubmers

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to check for overflow. Fixed.

case M_INFO_TOPIC_FRAGMENTATION:
return Allocator.getFragmentationInfo(reinterpret_cast<char *>(buffer),
size);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you want a default case here and below?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can rewrite with a default case. Is it better?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it's better to warn any invalid options than silent it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Now it prints a warning message.

@piwicode piwicode force-pushed the main branch 3 times, most recently from c347614 to 3a4578e Compare June 6, 2025 15:50
@ChiaHungDuan
Copy link
Contributor

Sorry I may not say this clearly. It's better to have copyToBuffer follow the new convention in this change and have another CL to fix the uses in getStats, .etc.

Also make possible to get the fragmentation stats from the primary
allocator.

Currenty, `__scudo_print_stats` symbol writes the report to the provided
printer, which is not convenient for processing the result.
@piwicode
Copy link
Author

Thanks for the review. Please take another look.

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

Successfully merging this pull request may close these issues.

5 participants