GWP-ASan

GWP-ASan เป็นฟีเจอร์ตัวจัดสรรหน่วยความจำของระบบที่ช่วยค้นหา ข้อบกพร่องของการใช้งานหลังช่วงใช้ฟรี (Use After Free) และ บัฟเฟอร์ล้นฮีป (Heap Buffer Overflow) ชื่อไม่เป็นทางการคือตัวย่อแบบเรียกซ้ำ"GWP-ASan Will Provide Allocation SANity" GWP-ASan ไม่ต้องใช้ซอร์สโค้ดหรือการคอมไพล์ใหม่ (นั่นคือทำงานร่วมกับ Prebuilt) และทำงานได้ทั้งในกระบวนการ 32 บิตและ 64 บิต (แม้ว่าข้อขัดข้อง 32 บิต จะมี ข้อมูลการแก้ไขข้อบกพร่องน้อยกว่า) ซึ่งแตกต่างจาก HWASan หรือ การแก้ไขข้อบกพร่องด้วย Malloc หัวข้อนี้จะอธิบายขั้นตอนที่คุณต้องดำเนินการเพื่อเปิดใช้ฟีเจอร์นี้ในแอป โดย GWP-ASan พร้อมใช้งานในแอปที่กำหนดเป้าหมายเป็น Android 11 (ระดับ API 30) ขึ้นไป

ภาพรวม

ระบบจะเปิดใช้ GWP-ASan ในแอปพลิเคชันระบบและไฟล์ปฏิบัติการของแพลตฟอร์มที่เลือกแบบสุ่มเมื่อเริ่มต้นกระบวนการ (หรือเมื่อ Zygote แยกออก) เปิดใช้ GWP-ASan ใน แอปของคุณเองเพื่อช่วยค้นหาข้อบกพร่องที่เกี่ยวข้องกับหน่วยความจำ และเตรียมแอปให้พร้อมรองรับ ARM Memory Tagging Extension (MTE) กลไกการสุ่มตัวอย่างการจัดสรรยังช่วยให้มั่นใจได้ถึงความน่าเชื่อถือในการป้องกัน การสืบค้นข้อมูลที่อาจทำให้เกิดข้อผิดพลาด

เมื่อเปิดใช้แล้ว GWP-ASan จะสกัดกั้นการจัดสรรฮีปซึ่งเป็นชุดย่อยที่เลือกแบบสุ่ม และวางไว้ในพื้นที่พิเศษที่ตรวจพบข้อบกพร่องของการเสียหายของหน่วยความจำฮีปที่ตรวจพบได้ยาก หากมีผู้ใช้จำนวนมากพอ แม้ว่าอัตราการสุ่มตัวอย่างจะต่ำ แต่ GWP-ASan ก็จะค้นพบข้อบกพร่องด้านความปลอดภัยของหน่วยความจำฮีปที่การทดสอบปกติไม่พบ ตัวอย่างเช่น GWP-ASan พบ ข้อบกพร่องจำนวนมาก ในเบราว์เซอร์ Chrome (ซึ่งหลายรายการยังอยู่ภายใต้การดูแบบจำกัด)

GWP-ASan จะรวบรวมข้อมูลเพิ่มเติมเกี่ยวกับการจัดสรรทั้งหมดที่สกัดกั้น ข้อมูลนี้จะพร้อมใช้งานเมื่อ GWP-ASan ตรวจพบการละเมิดความปลอดภัยของหน่วยความจำ และระบบจะวางข้อมูลลงในรายงานข้อขัดข้องของระบบโดยอัตโนมัติ ซึ่งช่วยในการแก้ไขข้อบกพร่องได้อย่างมาก (ดูตัวอย่าง)

GWP-ASan ออกแบบมาเพื่อไม่ให้เกิดค่าใช้จ่าย CPU ที่สำคัญ GWP-ASan จะเพิ่มค่าใช้จ่าย RAM ขนาดเล็กแบบคงที่เมื่อเปิดใช้ ระบบ Android เป็นผู้กำหนดค่าใช้จ่ายนี้ ซึ่งปัจจุบันอยู่ที่ประมาณ 70 กิบิไบต์ (KiB) สำหรับแต่ละกระบวนการที่ได้รับผลกระทบ

เลือกใช้ GWP-ASan ในแอป

แอปอาจเปิดใช้ GWP-ASan ในระดับต่อกระบวนการได้โดยใช้แท็ก android:gwpAsanMode ในไฟล์ Manifest ของแอป ระบบรองรับตัวเลือกต่อไปนี้

  • ปิดใช้เสมอ (android:gwpAsanMode="never"): การตั้งค่านี้จะปิดใช้ GWP-ASan ในแอปของคุณโดยสมบูรณ์ และเป็นค่าเริ่มต้นสำหรับแอปที่ไม่ใช่ระบบ

  • ค่าเริ่มต้น (android:gwpAsanMode="default" หรือไม่ได้ระบุ): Android 13 (API ระดับ 33) และต่ำกว่า - GWP-ASan จะปิดใช้ Android 14 (ระดับ API 34) และ ขึ้นไป - ระบบจะเปิดใช้ GWP-ASan ที่กู้คืนได้

  • เปิดใช้เสมอ (android:gwpAsanMode="always"): การตั้งค่านี้จะเปิดใช้ GWP-ASan ในแอปของคุณ ซึ่งรวมถึงสิ่งต่อไปนี้

    1. ระบบปฏิบัติการจะจอง RAM จำนวนหนึ่งไว้สำหรับการดำเนินการ GWP-ASan ซึ่งอยู่ที่ประมาณ ~70KiB สำหรับแต่ละกระบวนการที่ได้รับผลกระทบ (เปิดใช้ GWP-ASan หากแอปของคุณไม่ได้รับผลกระทบอย่างรุนแรงจากการเพิ่มขึ้นของการใช้หน่วยความจำ)

    2. GWP-ASan จะสกัดกั้นการจัดสรรฮีปซึ่งเป็นชุดย่อยที่เลือกแบบสุ่ม และวางไว้ในพื้นที่พิเศษที่ตรวจพบการละเมิดความปลอดภัยของหน่วยความจำได้อย่างน่าเชื่อถือ

    3. เมื่อเกิดการละเมิดความปลอดภัยของหน่วยความจำในพื้นที่พิเศษ GWP-ASan จะสิ้นสุดกระบวนการ

    4. GWP-ASan จะให้ข้อมูลเพิ่มเติมเกี่ยวกับข้อผิดพลาดในรายงานข้อขัดข้อง

หากต้องการเปิดใช้ GWP-ASan ทั่วโลกสำหรับแอป ให้เพิ่มโค้ดต่อไปนี้ลงในไฟล์ AndroidManifest.xml

<application android:gwpAsanMode="always">
  ...
</application>

นอกจากนี้ คุณยังเปิดใช้หรือปิดใช้ GWP-ASan อย่างชัดเจนสำหรับกระบวนการย่อยที่เฉพาะเจาะจงของแอปได้ โดยสามารถกำหนดเป้าหมายกิจกรรมและบริการโดยใช้กระบวนการที่เลือกใช้หรือไม่เลือกใช้ GWP-ASan อย่างชัดเจน ดูตัวอย่างได้ที่ด้านล่าง

<application>
  <processes>
    <!-- Create the (empty) application process -->
    <process />

    <!-- Create subprocesses with GWP-ASan both explicitly enabled and disabled. -->
    <process android:process=":gwp_asan_enabled"
               android:gwpAsanMode="always" />
    <process android:process=":gwp_asan_disabled"
               android:gwpAsanMode="never" />
  </processes>

  <!-- Target services and activities to be run on either the GWP-ASan enabled or disabled processes. -->
  <activity android:name="android.gwpasan.GwpAsanEnabledActivity"
            android:process=":gwp_asan_enabled" />
  <activity android:name="android.gwpasan.GwpAsanDisabledActivity"
            android:process=":gwp_asan_disabled" />
  <service android:name="android.gwpasan.GwpAsanEnabledService"
           android:process=":gwp_asan_enabled" />
  <service android:name="android.gwpasan.GwpAsanDisabledService"
           android:process=":gwp_asan_disabled" />
</application>

GWP-ASan ที่กู้คืนได้

Android 14 (ระดับ API 34) ขึ้นไปรองรับ Recoverable GWP-ASan ซึ่งช่วยให้นักพัฒนาซอฟต์แวร์ค้นหาข้อบกพร่องของบัฟเฟอร์ล้นฮีป (Heap Buffer Overflow) และการใช้งานฮีปหลังช่วงใช้ฟรี (Heap Use After Free) ในเวอร์ชันที่ใช้งานจริงโดยไม่ทำให้ประสบการณ์ของผู้ใช้แย่ลง เมื่อไม่ได้ระบุ android:gwpAsanMode ใน AndroidManifest.xml แอปจะใช้ GWP-ASan ที่กู้คืนได้

GWP-ASan ที่กู้คืนได้แตกต่างจาก GWP-ASan พื้นฐานดังนี้

  1. GWP-ASan ที่กู้คืนได้จะเปิดใช้เฉพาะเมื่อเปิดแอปประมาณ 1% เท่านั้น ไม่ใช่ทุกครั้งที่เปิดแอปพลิเคชัน
  2. เมื่อตรวจพบข้อบกพร่องของการใช้งานฮีปหลังช่วงใช้ฟรี (Heap Use After Free) หรือบัฟเฟอร์ล้นฮีป (Heap Buffer Overflow) ข้อบกพร่องนี้จะปรากฏในรายงานข้อขัดข้อง (Tombstone) รายงานข้อขัดข้องนี้พร้อมใช้งาน ผ่าน ActivityManager#getHistoricalProcessExitReasons API เช่นเดียวกับ GWP-ASan เดิม
  3. GWP-ASan ที่กู้คืนได้จะอนุญาตให้เกิดการเสียหายของหน่วยความจำและแอปทำงานต่อไปแทนที่จะออกหลังจากทิ้งรายงานข้อขัดข้อง แม้ว่ากระบวนการอาจดำเนินต่อไปตามปกติ แต่ลักษณะการทำงานของแอปจะไม่ได้รับการระบุอีกต่อไป เนื่องจากการเสียหายของหน่วยความจำ แอปอาจขัดข้องในบางจุดในอนาคต หรืออาจทำงานต่อไปโดยไม่มีผลกระทบที่ผู้ใช้มองเห็น
  4. ระบบจะปิดใช้ GWP-ASan ที่กู้คืนได้หลังจากทิ้งรายงานข้อขัดข้อง ดังนั้น แอปจึงรับรายงาน GWP-ASan ที่กู้คืนได้เพียงรายงานเดียวต่อการเปิดแอป
  5. หากมีการติดตั้งตัวจัดการสัญญาณที่กำหนดเองในแอป ระบบจะไม่เรียกใช้ตัวจัดการสัญญาณดังกล่าวสำหรับสัญญาณ SIGSEGV ที่บ่งบอกถึงข้อผิดพลาดของ GWP-ASan ที่กู้คืนได้

เนื่องจากข้อขัดข้องของ GWP-ASan ที่กู้คืนได้บ่งบอกถึงอินสแตนซ์จริงของการเสียหายของหน่วยความจำในอุปกรณ์ของผู้ใช้ปลายทาง เราจึงขอแนะนำอย่างยิ่งให้จัดลำดับความสำคัญและแก้ไขข้อบกพร่องที่ระบุโดย GWP-ASan ที่กู้คืนได้

การสนับสนุนสำหรับนักพัฒนาซอฟต์แวร์

ส่วนเหล่านี้จะอธิบายปัญหาที่อาจเกิดขึ้นเมื่อใช้ GWP-ASan และวิธีแก้ไขปัญหา

ไม่มีการติดตามการจัดสรร/ยกเลิกการจัดสรร

หากคุณกำลังวินิจฉัยข้อขัดข้องของระบบที่ดูเหมือนว่าไม่มี เฟรมการจัดสรร/ยกเลิกการจัดสรร แอปพลิเคชันของคุณอาจไม่มี ตัวชี้เฟรม GWP-ASan ใช้ตัวชี้เฟรมเพื่อบันทึกการติดตามการจัดสรรและการยกเลิกการจัดสรรด้วยเหตุผลด้านประสิทธิภาพ และไม่สามารถยกเลิกการติดตามสแต็กเทรซได้หากไม่มีตัวชี้เฟรม

ตัวชี้เฟรมจะเปิดอยู่โดยค่าเริ่มต้นสำหรับอุปกรณ์ arm64 และปิดอยู่โดยค่าเริ่มต้นสำหรับอุปกรณ์ arm32 เนื่องจากแอปพลิเคชันไม่มีการควบคุม libc GWP-ASan จึงไม่สามารถรวบรวมการติดตามการจัดสรร/ยกเลิกการจัดสรรสำหรับไฟล์ปฏิบัติการหรือแอป 32 บิตได้ (โดยทั่วไป) แอปพลิเคชัน 64 บิตควรตรวจสอบว่าไม่ได้สร้างด้วย not built with -fomit-frame-pointer เพื่อให้ GWP-ASan รวบรวมการติดตามสแต็กของการจัดสรรและ การยกเลิกการจัดสรรได้

การจำลองการละเมิดความปลอดภัย

GWP-ASan ออกแบบมาเพื่อตรวจจับการละเมิดความปลอดภัยของหน่วยความจำฮีปในอุปกรณ์ของผู้ใช้ GWP-ASan จะให้ข้อมูลบริบทเกี่ยวกับข้อขัดข้องมากที่สุดเท่าที่จะทำได้ (การติดตามการเข้าถึงของการละเมิด สตริงสาเหตุ และการติดตามการจัดสรร/ยกเลิกการจัดสรร) แต่ก็อาจยังคงยากที่จะอนุมานว่าการละเมิดเกิดขึ้นได้อย่างไร น่าเสียดายที่การตรวจหาข้อบกพร่องเป็นแบบน่าจะเป็น ดังนั้นรายงาน GWP-ASan จึงมักจะจำลองได้ยากในอุปกรณ์ในเครื่อง

ในกรณีเหล่านี้ หากข้อบกพร่องส่งผลต่ออุปกรณ์ 64 บิต คุณควรใช้ HWAddressSanitizer (HWASan) HWASan ตรวจจับการละเมิดความปลอดภัยของหน่วยความจำในสแต็ก ฮีป และส่วนกลางได้อย่างน่าเชื่อถือ การเรียกใช้แอปพลิเคชันด้วย HWASan อาจจำลองผลลัพธ์เดียวกันกับที่ GWP-ASan รายงานได้อย่างน่าเชื่อถือ

ในกรณีที่การเรียกใช้แอปพลิเคชันภายใต้ HWASan ไม่เพียงพอที่จะ หาสาเหตุของข้อบกพร่อง คุณควรลอง Fuzz โค้ด ที่เป็นปัญหา คุณสามารถกำหนดเป้าหมายความพยายามในการ Fuzz ตามข้อมูลในรายงาน GWP-ASan ซึ่งตรวจจับและแสดงปัญหาด้านความสมบูรณ์ของโค้ดที่ซ่อนอยู่ได้อย่างน่าเชื่อถือ

ตัวอย่าง

โค้ดแบบเนทีฟตัวอย่างนี้มีข้อบกพร่องของการใช้งานฮีปหลังช่วงใช้ฟรี (Heap Use After Free)

#include <jni.h>
#include <string>
#include <string_view>

jstring native_get_string(JNIEnv* env) {
   std::string s = "Hellooooooooooooooo ";
   std::string_view sv = s + "World\n";

   // BUG: Use-after-free. `sv` holds a dangling reference to the ephemeral
   // string created by `s + "World\n"`. Accessing the data here is a
   // use-after-free.
   return env->NewStringUTF(sv.data());
}

extern "C" JNIEXPORT jstring JNICALL
Java_android11_test_gwpasan_MainActivity_nativeGetString(
    JNIEnv* env, jobject /* this */) {
  // Repeat the buggy code a few thousand times. GWP-ASan has a small chance
  // of detecting the use-after-free every time it happens. A single user who
  // triggers the use-after-free thousands of times will catch the bug once.
  // Alternatively, if a few thousand users each trigger the bug a single time,
  // you'll also get one report (this is the assumed model).
  jstring return_string;
  for (unsigned i = 0; i < 0x10000; ++i) {
    return_string = native_get_string(env);
  }

  return reinterpret_cast<jstring>(env->NewGlobalRef(return_string));
}

สำหรับการทดสอบโดยใช้โค้ดตัวอย่างข้างต้น GWP-ASan ตรวจพบการใช้งานที่ผิดกฎหมายและทริกเกอร์รายงานข้อขัดข้องด้านล่างได้สำเร็จ GWP-ASan ได้ปรับปรุงรายงานโดยอัตโนมัติด้วยการให้ข้อมูลเกี่ยวกับประเภทของข้อขัดข้อง ข้อมูลเมตาของการจัดสรร และการติดตามสแต็กของการจัดสรรและการยกเลิกการจัดสรรที่เกี่ยวข้อง

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/sargo/sargo:10/RPP3.200320.009/6360804:userdebug/dev-keys'
Revision: 'PVT1.0'
ABI: 'arm64'
Timestamp: 2020-04-06 18:27:08-0700
pid: 16227, tid: 16227, name: 11.test.gwpasan  >>> android11.test.gwpasan <<<
uid: 10238
signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x736ad4afe0
Cause: [GWP-ASan]: Use After Free on a 32-byte allocation at 0x736ad4afe0

backtrace:
      #00 pc 000000000037a090  /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::CheckNonHeapValue(char, art::(anonymous namespace)::JniValueType)+448)
      #01 pc 0000000000378440  /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::CheckPossibleHeapValue(art::ScopedObjectAccess&, char, art::(anonymous namespace)::JniValueType)+204)
      #02 pc 0000000000377bec  /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::Check(art::ScopedObjectAccess&, bool, char const*, art::(anonymous namespace)::JniValueType*)+612)
      #03 pc 000000000036dcf4  /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::CheckJNI::NewStringUTF(_JNIEnv*, char const*)+708)
      #04 pc 000000000000eda4  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (_JNIEnv::NewStringUTF(char const*)+40)
      #05 pc 000000000000eab8  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (native_get_string(_JNIEnv*)+144)
      #06 pc 000000000000edf8  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (Java_android11_test_gwpasan_MainActivity_nativeGetString+44)
      ...

deallocated by thread 16227:
      #00 pc 0000000000048970  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan::AllocationMetadata::CallSiteInfo::RecordBacktrace(unsigned long (*)(unsigned long*, unsigned long))+80)
      #01 pc 0000000000048f30  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan::GuardedPoolAllocator::deallocate(void*)+184)
      #02 pc 000000000000f130  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (std::__ndk1::_DeallocateCaller::__do_call(void*)+20)
      ...
      #08 pc 000000000000ed6c  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (std::__ndk1::basic_string<char, std::__ndk1::char_traits<char>, std::__ndk1::allocator<char> >::~basic_string()+100)
      #09 pc 000000000000ea90  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (native_get_string(_JNIEnv*)+104)
      #10 pc 000000000000edf8  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (Java_android11_test_gwpasan_MainActivity_nativeGetString+44)
      ...

allocated by thread 16227:
      #00 pc 0000000000048970  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan::AllocationMetadata::CallSiteInfo::RecordBacktrace(unsigned long (*)(unsigned long*, unsigned long))+80)
      #01 pc 0000000000048e4c  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan::GuardedPoolAllocator::allocate(unsigned long)+368)
      #02 pc 000000000003b258  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan_malloc(unsigned long)+132)
      #03 pc 000000000003bbec  /apex/com.android.runtime/lib64/bionic/libc.so (malloc+76)
      #04 pc 0000000000010414  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (operator new(unsigned long)+24)
      ...
      #10 pc 000000000000ea6c  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (native_get_string(_JNIEnv*)+68)
      #11 pc 000000000000edf8  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (Java_android11_test_gwpasan_MainActivity_nativeGetString+44)
      ...

ข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับรายละเอียดการติดตั้งใช้งาน GWP-ASan ได้ที่ เอกสารประกอบของ LLVM ดูข้อมูลเพิ่มเติมเกี่ยวกับรายงานข้อขัดข้องของระบบ Android ได้ที่ การวินิจฉัยข้อขัดข้องของระบบ