Có phải std :: ptr :: write chuyển các tập tin uninitialized-ness của các byte mà nó viết không?


8

Tôi đang làm việc trên một thư viện giúp giao dịch các loại phù hợp với kích thước con trỏ int trên các ranh giới FFI. Giả sử tôi có một cấu trúc như thế này:

use std::mem::{size_of, align_of};

struct PaddingDemo {
    data: u8,
    force_pad: [usize; 0]
}

assert_eq!(size_of::<PaddingDemo>(), size_of::<usize>());
assert_eq!(align_of::<PaddingDemo>(), align_of::<usize>());

Cấu trúc này có 1 byte dữ liệu và 7 byte đệm. Tôi muốn gói một thể hiện của cấu trúc này vào một usizevà sau đó giải nén nó ở phía bên kia của ranh giới FFI. Bởi vì thư viện này là chung chung, tôi đang sử dụng MaybeUninitptr::write:

use std::ptr;
use std::mem::MaybeUninit;

let data = PaddingDemo { data: 12, force_pad: [] };

// In order to ensure all the bytes are initialized,
// zero-initialize the buffer
let mut packed: MaybeUninit<usize> = MaybeUninit::zeroed();
let ptr = packed.as_mut_ptr() as *mut PaddingDemo;

let packed_int = unsafe {
    std::ptr::write(ptr, data);
    packed.assume_init()
};

// Attempt to trigger UB in Miri by reading the
// possibly uninitialized bytes
let copied = unsafe { ptr::read(&packed_int) };

assume_initcuộc gọi đó kích hoạt hành vi không xác định? Nói cách khác, khi các ptr::writebản sao cấu trúc vào bộ đệm, nó có sao chép trạng thái chưa được khởi tạo của các byte đệm, ghi đè trạng thái khởi tạo thành byte không?

Hiện tại, khi mã này hoặc mã tương tự được chạy trong Miri, nó không phát hiện bất kỳ Hành vi không xác định nào. Tuy nhiên, theo các cuộc thảo luận về vấn đề này trên github , ptr::writeđược cho là sao chép các byte đệm đó, và hơn nữa để sao chép tính chưa được khởi tạo của chúng. Điều đó có đúng không? Các tài liệu ptr::writekhông nói về điều này chút nào, cũng như phần nomicon trên bộ nhớ chưa được khởi tạo .


Một số tối ưu hóa hữu ích có thể được tạo điều kiện bằng cách sao chép giá trị không xác định rời khỏi đích ở trạng thái không xác định, nhưng đôi khi cần phải sao chép một đối tượng với ngữ nghĩa mà bất kỳ phần nào của bản gốc không xác định trở thành không được chỉ định trong bản sao (vì vậy mọi bản sao trong tương lai sẽ được đảm bảo khớp với nhau). Thật không may, các nhà thiết kế ngôn ngữ dường như không xem xét nhiều đến tầm quan trọng của việc có thể đạt được ngữ nghĩa sau trong mã nhạy cảm bảo mật.
supercat

Câu trả lời:


3

Có phải cuộc gọi giả định đó đã kích hoạt hành vi không xác định?

Đúng. "Uninitialized" chỉ là một giá trị khác mà một byte trong Máy Tóm tắt Rust có thể có, bên cạnh 0x00 - 0xFF thông thường. Hãy để chúng tôi viết byte đặc biệt này là 0xUU. (Xem bài đăng trên blog này để có thêm một chút nền tảng về chủ đề này .) 0xUU được bảo toàn bởi các bản sao giống như bất kỳ giá trị có thể nào khác mà một byte có thể có được giữ bởi các bản sao.

Nhưng các chi tiết phức tạp hơn một chút. Có hai cách để sao chép dữ liệu xung quanh trong bộ nhớ trong Rust. Thật không may, các chi tiết cho điều này cũng không được nhóm ngôn ngữ Rust quy định rõ ràng, vì vậy những gì diễn ra sau đây là cách giải thích cá nhân của tôi. Tôi nghĩ những gì tôi đang nói là không gây tranh cãi trừ khi được đánh dấu khác, nhưng tất nhiên đó có thể là một ấn tượng sai.

Unyped / byte-khôn ngoan sao chép

Nói chung, khi một phạm vi byte được sao chép, phạm vi nguồn sẽ ghi đè lên phạm vi đích - vì vậy nếu phạm vi nguồn là "0x00 0xUU 0xUU 0xUU", thì sau khi sao chép, phạm vi đích sẽ có danh sách byte chính xác đó.

Đây là những gì memcpy/ memmovetrong C hành xử như thế nào (theo cách giải thích của tôi về tiêu chuẩn, không rõ ràng ở đây không may). Trong Rust, ptr::copy{,_nonoverlapping} có thể thực hiện một bản sao theo byte, nhưng hiện tại nó không thực sự được chỉ định chính xác và một số người có thể muốn nói rằng nó cũng được gõ. Điều này đã được thảo luận một chút trong vấn đề này .

Bản đánh máy

Thay thế là một "bản sao được gõ", đó là những gì xảy ra trên mọi phép gán thông thường ( =) và khi truyền các giá trị đến / từ một hàm. Một bản sao được gõ sẽ giải thích bộ nhớ nguồn ở một số loại Tvà sau đó "tái tuần tự hóa" giá trị của loại đó Tvào bộ nhớ đích.

Sự khác biệt chính đối với bản sao theo byte là thông tin không liên quan ở loại Tbị mất. Về cơ bản, đây là một cách phức tạp để nói rằng một bản sao được gõ "quên" phần đệm và đặt lại nó thành không được khởi tạo một cách hiệu quả. So với một bản sao chưa được đánh dấu, một bản sao được đánh máy sẽ mất nhiều thông tin hơn. Các bản sao được đánh dấu giữ nguyên biểu diễn bên dưới, các bản sao được nhập chỉ bảo toàn giá trị được biểu diễn.

Vì vậy, ngay cả khi bạn chuyển đổi 0usizesang PaddingDemo, một bản sao được nhập của giá trị đó có thể đặt lại giá trị này thành "0x00 0xUU 0xUU 0xUU" (hoặc bất kỳ byte nào có thể khác cho phần đệm) - giả sử datanằm ở offset 0, không được bảo đảm (thêm #[repr(C)]nếu bạn muốn sự đảm bảo đó).

Trong trường hợp của bạn, ptr::writelấy một đối số kiểu PaddingDemovà đối số được truyền qua một bản sao được gõ. Vì vậy, tại thời điểm đó, các byte đệm có thể thay đổi tùy ý, đặc biệt chúng có thể trở thành 0xUU.

Chưa hoàn thành usize

Việc mã của bạn có UB hay không phụ thuộc vào một yếu tố khác, cụ thể là việc có một byte chưa được khởi tạo trong một usizelà UB hay không. Câu hỏi là, một phạm vi bộ nhớ chưa được khởi tạo (một phần) đại diện cho một số nguyên? Hiện tại, nó không và do đó có UB . Tuy nhiên, liệu đó có phải là trường hợp được tranh luận nhiều hay không và dường như cuối cùng chúng ta sẽ cho phép nó.

Tuy nhiên, nhiều chi tiết khác vẫn chưa rõ ràng - ví dụ, việc chuyển "0x00 0xUU 0xUU 0xUU" sang một số nguyên cũng có thể dẫn đến một số nguyên chưa được khởi tạo hoàn toàn , nghĩa là các số nguyên có thể không thể bảo toàn "khởi tạo một phần". Để bảo toàn các byte được khởi tạo một phần trong các số nguyên, về cơ bản chúng ta phải nói rằng một số nguyên không có "giá trị" trừu tượng, nó chỉ là một chuỗi các byte (có thể chưa được khởi tạo). Điều này không phản ánh cách các số nguyên được sử dụng trong các hoạt động như thế nào /. (Một số điều này cũng phụ thuộc vào các quyết định LLVM xung quanh poisonfreeze ; LLVM có thể quyết định rằng khi thực hiện tải ở loại số nguyên, kết quả là hoàn toàn poisonnếu có bất kỳ byte đầu vào nàopoison.) Vì vậy, ngay cả khi mã không phải là UB vì chúng tôi cho phép các số nguyên chưa được khởi tạo, nó có thể không hoạt động như mong đợi vì dữ liệu bạn muốn chuyển đang bị mất.

Nếu bạn muốn chuyển byte thô xung quanh, tôi khuyên bạn nên sử dụng một loại phù hợp với điều đó, chẳng hạn như MaybeUninit. Nếu bạn sử dụng một kiểu số nguyên, mục tiêu sẽ là chuyển các giá trị nguyên - tức là số.


Đây là tất cả rất hữu ích, cảm ơn bạn!
Lucretiel

Vì vậy, theo giả thuyết, nếu hành vi được mô tả trong đoạn cuối của bạn được chính thức hóa (không phải là trường hợp ngay bây giờ), một kích thước có thể được phép có các byte UU miễn là không có thao tác nào được thực hiện trên đó, và sau đó được chuyển trở lại kiểu ban đầu của tôi, sẽ hoạt động vì không thành vấn đề nếu các byte đệm là UU.
Lucretiel

Cảm ơn các câu trả lời chi tiết! Miri có thể phát hiện ra loại hành vi không xác định này không?
Sven Marnach

1
@Lucretiel nếu chúng tôi quyết định usizeđại diện cho các túi byte (chứ không phải số nguyên), thì có, usizeMaybeUninit<usize>sẽ tương đương và cả hai sẽ bảo toàn hoàn hảo mức byte bên dưới (và điều này bao gồm cả biểu diễn "byte không xác định").
Ralf Jung

1
@SvenMarnach Bởi vì việc triển khai hiện tạiptr::write đủ thông minh để không sao chép các byte chưa được khởi tạo.
Lucretiel
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.