Tiện ích gắn thẻ bộ nhớ Arm

Arm v9 ra mắt Tiện ích gắn thẻ bộ nhớ (MTE) Arm, một cách triển khai phần cứng của bộ nhớ được gắn thẻ.

Ở cấp cao, MTE gắn thẻ cho từng lượt phân bổ/giải phóng bộ nhớ bằng siêu dữ liệu bổ sung. Phương thức này gán một thẻ cho một vị trí bộ nhớ, sau đó có thể được liên kết với các con trỏ tham chiếu đến vị trí bộ nhớ đó. Trong thời gian chạy, CPU sẽ kiểm tra để đảm bảo con trỏ và thẻ siêu dữ liệu khớp nhau trên mỗi lần tải và lưu trữ.

Trong Android 12, trình phân bổ bộ nhớ vùng nhớ khối xếp của hạt nhân và không gian người dùng có thể tăng cường mỗi lượt phân bổ bằng siêu dữ liệu. Điều này giúp phát hiện lỗi use-after-free và lỗi buffer-overflow. Đây là nguồn lỗi về độ an toàn của bộ nhớ phổ biến nhất trong cơ sở mã của chúng tôi.

Các chế độ hoạt động của MTE

MTE có 3 chế độ hoạt động:

  • Chế độ đồng bộ (SYNC)
  • Chế độ không đồng bộ (ASYNC)
  • Chế độ không đối xứng (ASYMM)

Chế độ đồng bộ (SYNC)

Chế độ này được tối ưu hoá cho độ chính xác của việc phát hiện lỗi hơn là cho hiệu suất. Bạn có thể dùng chế độ này làm công cụ phát hiện lỗi chính xác khi mức hao tổn hiệu suất cao hơn được chấp nhận. Khi được bật, chế độ SYNC của MTE sẽ hoạt động như một giải pháp giảm thiểu bảo mật. Khi một thẻ không khớp, bộ xử lý sẽ huỷ ngay quá trình thực thi và chấm dứt quy trình bằng SIGSEGV (mã SEGV_MTESERR) cùng với đầy đủ thông tin về quyền truy cập vào bộ nhớ và địa chỉ bị lỗi.

Bạn nên sử dụng chế độ này trong quá trình kiểm thử thay cho HWASan/KASAN hoặc trong phiên bản chính thức khi quy trình mục tiêu cho thấy một nền tảng dễ bị tấn công. Ngoài ra, khi chế độ ASYNC cho biết có lỗi, bạn có thể lấy được báo cáo lỗi chính xác bằng cách sử dụng API thời gian chạy để chuyển đổi quá trình thực thi sang chế độ SYNC.

Khi chạy ở chế độ SYNC, trình phân bổ Android sẽ ghi lại dấu vết ngăn xếp cho tất cả các lượt phân bổ và giải phóng và sử dụng các dấu vết đó để cung cấp báo cáo lỗi tốt hơn, bao gồm cả nội dung giải thích về lỗi bộ nhớ, chẳng hạn như use-after-free hoặc buffer-overflow và dấu vết ngăn xếp của các sự kiện bộ nhớ có liên quan. Những báo cáo như vậy cung cấp nhiều thông tin theo ngữ cảnh hơn, đồng thời giúp việc theo dõi và khắc phục lỗi dễ dàng hơn.

Chế độ không đồng bộ (ASYNC)

Chế độ này được tối ưu hoá cho hiệu suất hơn là cho độ chính xác của báo cáo lỗi. Bạn có thể dùng chế độ này để phát hiện lỗi về độ an toàn của bộ nhớ đó ở mức hao tổn thấp.
Khi một thẻ không khớp, bộ xử lý sẽ tiếp tục quá trình thực thi cho đến mục nhân hệ điều hành gần nhất (ví dụ: gián đoạn lệnh gọi hệ thống hoặc bộ tính giờ), trong đó bộ xử lý chấm dứt quy trình này bằng SIGSEGV (mã SEGV_MTEAERR) mà không ghi lại địa chỉ bị lỗi hoặc quyền truy cập vào bộ nhớ.
Bạn nên sử dụng chế độ này trong phiên bản chính thức trên các cơ sở mã được kiểm thử kỹ lưỡng, trong đó mật độ lỗi về độ an toàn của bộ nhớ được biết đến là thấp, đạt được bằng cách sử dụng chế độ SYNC trong quá trình kiểm thử.

Chế độ không đối xứng (ASYMM)

Một tính năng bổ sung trong Arm v8.7-A, chế độ MTE không đối xứng cung cấp tính năng kiểm tra đồng bộ đối với hoạt động đọc bộ nhớ và kiểm tra không đồng bộ đối với hoạt động ghi bộ nhớ, với hiệu suất tương tự như chế độ ASYNC. Trong hầu hết các trường hợp, chế độ này là một điểm cải tiến so với chế độ ASYNC và bạn nên sử dụng chế độ này thay vì chế độ ASYNC bất cứ khi nào có thể.

Vì lý do này, không có API nào được mô tả bên dưới đề cập đến chế độ Không đối xứng. Thay vào đó, bạn có thể định cấu hình hệ điều hành để luôn sử dụng chế độ Không đối xứng khi yêu cầu Không đồng bộ. Vui lòng tham khảo phần "Định cấu hình cấp MTE ưu tiên dành riêng cho CPU" để biết thêm thông tin.

MTE trong không gian người dùng

Các phần sau đây mô tả cách bật MTE cho các quy trình hệ thống và ứng dụng. Theo mặc định, MTE bị tắt, trừ phi bạn đặt một trong các tuỳ chọn bên dưới cho một quy trình cụ thể (xem các thành phần mà MTE được bật dưới đây).

Bật MTE bằng hệ thống xây dựng

Là một thuộc tính trên toàn quy trình, MTE được kiểm soát bằng chế độ cài đặt thời gian tạo bản dựng của tệp thực thi chính. Các tuỳ chọn sau đây cho phép thay đổi chế độ cài đặt này cho từng tệp thực thi hoặc cho toàn bộ thư mục con trong cây nguồn. Chế độ cài đặt này sẽ bị bỏ qua trên các thư viện hoặc bất kỳ mục tiêu nào không phải là tệp thực thi cũng không phải là kiểm thử.

1. Bật MTE trong Android.bp (ví dụ) cho một dự án cụ thể:

Chế độ MTE Xem xét
MTE không đồng bộ
  sanitize: {
  memtag_heap: true,
  }
MTE đồng bộ
  sanitize: {
  memtag_heap: true,
  diag: {
  memtag_heap: true,
  },
  }

hoặc trong Android.mk:

Chế độ MTE Xem xét
Asynchronous MTE LOCAL_SANITIZE := memtag_heap
Synchronous MTE LOCAL_SANITIZE := memtag_heap
LOCAL_SANITIZE_DIAG := memtag_heap

2. Bật MTE trên một thư mục con trong cây nguồn bằng cách sử dụng biến sản phẩm:

Chế độ MTE Danh sách bao gồm Danh sách loại trừ
không đồng bộ PRODUCT_MEMTAG_HEAP_ASYNC_INCLUDE_PATHS MEMTAG_HEAP_ASYNC_INCLUDE_PATHS PRODUCT_MEMTAG_HEAP_EXCLUDE_PATHS MEMTAG_HEAP_EXCLUDE_PATHS
đồng bộ hóa PRODUCT_MEMTAG_HEAP_SYNC_INCLUDE_PATHS MEMTAG_HEAP_SYNC_INCLUDE_PATHS

hoặc

Chế độ MTE Xem xét
MTE không đồng bộ MEMTAG_HEAP_ASYNC_INCLUDE_PATHS
MTE đồng bộ MEMTAG_HEAP_SYNC_INCLUDE_PATHS

hoặc bằng cách chỉ định đường dẫn loại trừ của tệp thực thi:

Chế độ MTE Xem xét
MTE không đồng bộ PRODUCT_MEMTAG_HEAP_EXCLUDE_PATHS MEMTAG_HEAP_EXCLUDE_PATHS
MTE đồng bộ

Ví dụ: (cách sử dụng tương tự như PRODUCT_CFI_INCLUDE_PATHS)

  PRODUCT_MEMTAG_HEAP_SYNC_INCLUDE_PATHS=vendor/$(vendor)
  PRODUCT_MEMTAG_HEAP_EXCLUDE_PATHS=vendor/$(vendor)/projectA \
                                    vendor/$(vendor)/projectB

Bật MTE bằng các thuộc tính hệ thống

Bạn có thể ghi đè các chế độ cài đặt bản dựng ở trên trong thời gian chạy bằng cách đặt thuộc tính hệ thống sau:

arm64.memtag.process.<basename> = (off|sync|async)

Trong đó basename là tên cơ sở của tệp thực thi.

Ví dụ: để đặt /system/bin/ping hoặc /data/local/tmp/ping sử dụng MTE không đồng bộ, hãy sử dụng adb shell setprop arm64.memtag.process.ping async.

Bật MTE bằng biến môi trường

Một cách khác để ghi đè chế độ cài đặt bản dựng là xác định biến môi trường: MEMTAG_OPTIONS=(off|sync|async) Nếu cả biến môi trường và thuộc tính hệ thống đều được xác định, thì biến sẽ được ưu tiên.

Bật MTE cho ứng dụng

Nếu không được chỉ định, MTE sẽ bị tắt theo mặc định, nhưng nếu muốn sử dụng MTE, ứng dụng có thể đặt android:memtagMode bên dưới thẻ <application> hoặc <process> trong AndroidManifest.xml.

android:memtagMode=(off|default|sync|async)

Khi được đặt trên thẻ <application>, thuộc tính này ảnh hưởng đến tất cả các quy trình mà ứng dụng dùng, đồng thời có thể bị ghi đè cho từng quy trình riêng bằng cách đặt thẻ <process>.

Để thử nghiệm, bạn có thể dùng các thay đổi về khả năng tương thích để đặt giá trị mặc định của thuộc tính memtagMode cho một ứng dụng không chỉ định giá trị nào trong tệp kê khai (hoặc chỉ định default).
Bạn có thể tìm thấy các thay đổi này trong System > Advanced > Developer options > App Compatibility Changes trong trình đơn cài đặt chung. Việc đặt NATIVE_MEMTAG_ASYNC hoặc NATIVE_MEMTAG_SYNC sẽ bật MTE cho một ứng dụng cụ thể.
Ngoài ra, bạn có thể đặt chúng bằng lệnh am như sau:

$ adb shell am compat enable NATIVE_MEMTAG_[A]SYNC my.app.name

Tạo hình ảnh hệ thống MTE

Bạn nên bật MTE trên tất cả tệp nhị phân gốc trong quá trình phát triển và khởi động. Điều này giúp phát hiện sớm các lỗi về an toàn bộ nhớ và cung cấp phạm vi người dùng thực tế, nếu được bật trong các bản dựng thử nghiệm.

Bạn nên bật MTE ở chế độ Đồng bộ trên tất cả tệp nhị phân gốc trong quá trình phát triển

SANITIZE_TARGET=memtag_heap SANITIZE_TARGET_DIAG=memtag_heap m

Giống như mọi biến trong hệ thống xây dựng, SANITIZE_TARGET có thể được dùng làm biến môi trường hoặc chế độ cài đặt make (ví dụ: trong tệp product.mk).
Xin lưu ý rằng việc này sẽ bật MTE cho tất cả các quy trình gốc, nhưng không phải cho các ứng dụng (được phân nhánh từ zygote64) mà bạn có thể bật MTE theo hướng dẫn ở trên.

Định cấu hình cấp MTE ưu tiên dành riêng cho CPU

Trên một số CPU, hiệu suất của MTE ở chế độ ASYMM hoặc thậm chí là SYNC có thể tương tự như hiệu suất của ASYNC. Điều này khiến bạn nên bật các bước kiểm tra nghiêm ngặt hơn trên các CPU đó khi yêu cầu chế độ kiểm tra ít nghiêm ngặt hơn, để có được các lợi ích phát hiện lỗi của các bước kiểm tra nghiêm ngặt hơn mà không bị giảm hiệu suất.
Theo mặc định, các quy trình được định cấu hình để chạy ở chế độ ASYNC sẽ chạy ở chế độ ASYNC trên tất cả CPU. Để định cấu hình hạt nhân chạy các quy trình này ở chế độ SYNC trên các CPU cụ thể, bạn phải ghi quá trình đồng bộ hoá giá trị vào mục nhập sysfs /sys/devices/system/cpu/cpu<N>/mte_tcf_preferred tại thời điểm khởi động. Bạn có thể thực hiện việc này bằng tập lệnh khởi động. Ví dụ: để định cấu hình CPU 0-1 chạy các quy trình ở chế độ ASYNC trong chế độ SYNC và CPU 2-3 chạy ở chế độ ASYMM, bạn có thể thêm nội dung sau vào mệnh đề khởi tạo của tập lệnh khởi tạo của nhà cung cấp:

  write /sys/devices/system/cpu/cpu0/mte_tcf_preferred sync
  write /sys/devices/system/cpu/cpu1/mte_tcf_preferred sync
  write /sys/devices/system/cpu/cpu2/mte_tcf_preferred asymm
  write /sys/devices/system/cpu/cpu3/mte_tcf_preferred asymm

Các mốc trong quá trình chế độ ASYNC chạy ở chế độ SYNC sẽ chứa dấu vết ngăn xếp chính xác về vị trí của lỗi bộ nhớ. Tuy nhiên, các tệp này sẽ không bao gồm dấu vết ngăn xếp phân bổ hoặc giải phóng. Các dấu vết ngăn xếp này chỉ có sẵn nếu quy trình được định cấu hình để chạy ở chế độ SYNC.

int mallopt(M_THREAD_DISABLE_MEM_INIT, level)

trong đó level là 0 hoặc 1.
Tắt tính năng khởi tạo bộ nhớ trong malloc và tránh thay đổi thẻ bộ nhớ trừ khi cần thiết để đảm bảo tính chính xác.

int mallopt(M_MEMTAG_TUNING, level)

trong đó level là:

  • M_MEMTAG_TUNING_BUFFER_OVERFLOW
  • M_MEMTAG_TUNING_UAF

Chọn chiến lược phân bổ thẻ.

  • Chế độ cài đặt mặc định là M_MEMTAG_TUNING_BUFFER_OVERFLOW.
  • M_MEMTAG_TUNING_BUFFER_OVERFLOW – cho phép phát hiện một cách xác định lỗi tràn và thiếu bộ đệm tuyến tính bằng cách gán các giá trị thẻ riêng biệt cho các lượt phân bổ liền kề. Chế độ này có khả năng phát hiện lỗi sử dụng sau khi giải phóng giảm đi một chút vì chỉ có một nửa số giá trị thẻ có thể có cho mỗi vị trí bộ nhớ. Xin lưu ý rằng MTE không thể phát hiện tình trạng tràn bộ nhớ trong cùng một hạt nhân thẻ (mảng được căn chỉnh 16 byte) và có thể bỏ lỡ các tình trạng tràn bộ nhớ nhỏ ngay cả ở chế độ này. Tình trạng tràn bộ nhớ như vậy không thể là nguyên nhân gây ra sự cố hỏng bộ nhớ, vì bộ nhớ trong một hạt không bao giờ được dùng cho nhiều lượt phân bổ.
  • M_MEMTAG_TUNING_UAF – bật các thẻ được tạo ngẫu nhiên độc lập với xác suất đồng nhất khoảng 93% để phát hiện cả lỗi không gian (tràn bộ đệm) và lỗi thời gian (sử dụng sau khi giải phóng).

Ngoài các API được mô tả ở trên, người dùng có kinh nghiệm nên lưu ý những điều sau:

  • Việc thiết lập thanh ghi phần cứng PSTATE.TCO có thể tạm thời ngăn chặn việc kiểm tra thẻ (ví dụ). Ví dụ: khi sao chép một dải bộ nhớ có nội dung thẻ không xác định hoặc giải quyết nút thắt cổ chai về hiệu suất trong một vòng lặp nóng.
  • Khi sử dụng M_HEAP_TAGGING_LEVEL_SYNC, trình xử lý sự cố hệ thống sẽ cung cấp thêm thông tin như dấu vết ngăn xếp phân bổ và giải phóng. Chức năng này yêu cầu quyền truy cập vào các bit thẻ và được bật bằng cách truyền cờ SA_EXPOSE_TAGBITS khi đặt trình xử lý tín hiệu. Mọi chương trình thiết lập trình xử lý tín hiệu riêng và uỷ quyền các sự cố không xác định cho trình xử lý tín hiệu hệ thống đều nên làm như vậy.

MTE trong nhân

Để bật KASAN tăng tốc MTE cho hạt nhân, hãy định cấu hình hạt nhân bằng CONFIG_KASAN=y, CONFIG_KASAN_HW_TAGS=y. Các cấu hình này được bật theo mặc định trên các hạt nhân GKI, bắt đầu từ Android 12-5.10.
Bạn có thể kiểm soát điều này tại thời điểm khởi động bằng các đối số dòng lệnh sau:

  • kasan=[on|off] – bật hoặc tắt KASAN (mặc định: on)
  • kasan.mode=[sync|async] – chọn giữa chế độ đồng bộ và không đồng bộ (mặc định: sync)
  • kasan.stacktrace=[on|off] – có thu thập dấu vết ngăn xếp hay không (mặc định: on)
    • tính năng thu thập dấu vết ngăn xếp cũng yêu cầu stack_depot_disable=off.
  • kasan.fault=[report|panic] – chỉ in báo cáo hay báo động nhân kernel (mặc định: report). Bất kể tuỳ chọn này là gì, tính năng kiểm tra thẻ sẽ bị tắt sau lỗi đầu tiên được báo cáo.

Bạn nên sử dụng chế độ SYNC trong quá trình khởi động, phát triển và kiểm thử. Bạn nên bật tuỳ chọn này trên toàn cục cho tất cả các quy trình bằng cách sử dụng biến môi trường hoặc với hệ thống xây dựng. Ở chế độ này, lỗi được phát hiện sớm trong quá trình phát triển, cơ sở mã được ổn định nhanh hơn và tránh được chi phí phát hiện lỗi sau này trong quá trình phát hành công khai.

Bạn nên sử dụng chế độ ASYNC trong môi trường sản xuất. Điều này cung cấp một công cụ có mức hao tổn thấp để phát hiện sự hiện diện của lỗi về độ an toàn của bộ nhớ trong một quy trình cũng như biện pháp phòng thủ chuyên sâu hơn. Sau khi phát hiện lỗi, nhà phát triển có thể tận dụng các API thời gian chạy để chuyển sang chế độ SYNC và nhận dấu vết ngăn xếp chính xác từ một nhóm người dùng được lấy mẫu.

Bạn nên định cấu hình cấp MTE ưu tiên dành riêng cho CPU cho SoC. Chế độ không đối xứng thường có các đặc điểm hiệu suất giống như ASYNC và hầu như luôn được ưu tiên hơn. Các nhân nhỏ theo thứ tự thường cho thấy hiệu suất tương tự nhau trong cả ba chế độ và có thể được định cấu hình để ưu tiên SYNC.

Nhà phát triển nên kiểm tra xem có sự cố hay không bằng cách kiểm tra /data/tombstones, logcat hoặc bằng cách theo dõi quy trình DropboxManager của nhà cung cấp để tìm lỗi người dùng cuối. Để biết thêm thông tin về cách gỡ lỗi mã gốc Android, hãy xem thông tin tại đây.

Các thành phần nền tảng đã bật MTE

Trong Android 12, một số thành phần hệ thống quan trọng về bảo mật sử dụng MTE ASYNC để phát hiện sự cố của người dùng cuối và đóng vai trò là một lớp bảo vệ bổ sung theo chiều sâu. Các thành phần này bao gồm:

  • Các trình nền và tiện ích mạng (ngoại trừ netd)
  • Bluetooth, SecureElement, HAL NFC và ứng dụng hệ thống
  • Trình nền statsd
  • system_server
  • zygote64 (để cho phép ứng dụng chọn sử dụng MTE)

Các mục tiêu này được chọn dựa trên các tiêu chí sau:

  • Một quy trình đặc quyền (được xác định là một quy trình có quyền truy cập vào một nội dung mà miền SELinux unprivileged_app không có)
  • Xử lý dữ liệu đầu vào không đáng tin cậy (Quy tắc hai)
  • Tình trạng chậm hiệu suất chấp nhận được (sự chậm trễ không tạo ra độ trễ mà người dùng có thể nhìn thấy)

Các nhà cung cấp nên bật MTE trong quá trình sản xuất cho nhiều thành phần hơn, theo các tiêu chí nêu trên. Trong quá trình phát triển, bạn nên kiểm thử các thành phần này bằng chế độ SYNC để phát hiện các lỗi dễ sửa và đánh giá tác động của ASYNC đối với hiệu suất của các thành phần đó.
Trong tương lai, Android dự định mở rộng danh sách các thành phần hệ thống được bật MTE, dựa trên các đặc điểm hiệu suất của các thiết kế phần cứng sắp ra mắt.