Skip to content
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

Potential unsoundness in libbpf-cargo generated code #589

Closed
danielocfb opened this issue Sep 28, 2023 · 3 comments · Fixed by #672
Closed

Potential unsoundness in libbpf-cargo generated code #589

danielocfb opened this issue Sep 28, 2023 · 3 comments · Fixed by #672
Assignees

Comments

@danielocfb
Copy link
Collaborator

I think there is possibility of unsoundness in the generated code, depending on what C code does. E.g., in the capable example we have the tool_config struct, which contains the uniqueness enumeration:

const volatile struct {
gid_t tgid; //PID to filter
bool verbose; // Include non audit logs
enum uniqueness unique_type; // Only unique info traces for same pid or cgroup
} tool_config = {};

enum uniqueness {
UNQ_OFF, UNQ_PID, UNQ_CGROUP
};

Once we generate the skeleton we end up with:

pub mod capable_rodata_types {
    #[derive(Debug, Copy, Clone)]
    #[repr(C)]
    pub struct rodata {
        pub tool_config: __anon_1,
    }
    #[derive(Debug, Default, Copy, Clone)]
    #[repr(C)]
    pub struct __anon_1 {
        pub tgid: u32,
        pub verbose: bool,
        pub unique_type: uniqueness,
    }
    #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
    #[repr(u32)]
    pub enum uniqueness {
        #[default]
        UNQ_OFF = 0,
        UNQ_PID = 1,
        UNQ_CGROUP = 2,
    }
}

and

pub fn rodata(&mut self) -> &'_ mut capable_rodata_types::rodata {
    unsafe {
        std::mem::transmute::<*mut std::ffi::c_void, &'_ mut capable_rodata_types::rodata>(
            self.skel_config.map_mmap_ptr(2).unwrap(),
        )
    }
}

That cast, however, is anything but safe. Rust's uniqueness requires that it has only any of the variant values 0, 1, or 2. But in C code it is fine to assign, say, 0xff to the enum object (without triggering UB, I believe). The result would be UB in Rust.

A similar thing can be observed for bool, though it may be that both languages have the same bool representation (questionable, because C leaves binary representation mostly undefined, I believe, but if it's all LLVM we probably luck out).

My reading is that this is basically a variant of rust-lang/rust#36927 (though I haven't gone through the issue in its entirety).

So at the very least we should make rodata (and similar methods) unsafe and document that it's on users. The better approach is probably to translate enums to constants on the Rust side, which I understand is done for a lot of foreign function interfaces.

@danielocfb
Copy link
Collaborator Author

Perhaps the most sensible solution is to wrap all enum instances in MaybeUninit. That moves the burden of check to the user, it is happening at the lowest level, which makes it clear what invariants need to be upheld (as opposed to making, say, bss an unsafe function), and it is the least intrusive (it would only affect the types that we identify as problematic).

@d-e-s-o
Copy link
Collaborator

d-e-s-o commented Oct 28, 2023

char is another type that is not valid for all bit patterns.

  |     let c = unsafe { *(values.1.as_ptr() as *const u8).cast::<char>() };
  |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |                      constructing invalid value: encountered 0x00110000,
  |                      but expected a valid unicode scalar value
  |                      (in `0..=0x10FFFF` but not in `0xD800..=0xDFFF`)

@danielocfb danielocfb self-assigned this Feb 6, 2024
@d-e-s-o
Copy link
Collaborator

d-e-s-o commented Feb 9, 2024

The char point is probably moot, because I don't believe we ever generate a char attribute in a skeleton. But for enums and bools, I found a way to wrap them in MaybeUninit, which I still believe to represent the best overall trade off.

d-e-s-o added a commit to d-e-s-o/libbpf-rs that referenced this issue Feb 9, 2024
Certain Rust types that we use in our generated skeleton are not valid
for all bit patterns. Bools, for example, are defined to be one byte in
size, but their representation allows only for 0 and 1 values to be used
in said byte -- everything else is undefined behavior. Enums have
similar problems, in that not the entire space of the backing type is
necessarily used by variants.
The result is an unsoundness issue in our generated skeletons, because C
code could accidentally set an enum to an invalid value and Rust code
would exhibit undefined behavior.
This change fixes this problem by wrapping the corresponding attributes
in MaybeUninit. In so doing users have to be explicit when accessing
them and make sure that the current representation is indeed valid.

Closes: libbpf#589
Signed-off-by: Daniel Müller <[email protected]>
d-e-s-o added a commit to d-e-s-o/libbpf-rs that referenced this issue Feb 12, 2024
Certain Rust types that we use in our generated skeleton are not valid
for all bit patterns. Bools, for example, are defined to be one byte in
size, but their representation allows only for 0 and 1 values to be used
in said byte -- everything else is undefined behavior. Enums have
similar problems, in that not the entire space of the backing type is
necessarily used by variants.
The result is an unsoundness issue in our generated skeletons, because C
code could accidentally set an enum to an invalid value and Rust code
would exhibit undefined behavior.
This change fixes this problem by wrapping the corresponding attributes
in MaybeUninit. In so doing users have to be explicit when accessing
them and make sure that the current representation is indeed valid.

Closes: libbpf#589
Signed-off-by: Daniel Müller <[email protected]>
danielocfb pushed a commit that referenced this issue Feb 13, 2024
Certain Rust types that we use in our generated skeleton are not valid
for all bit patterns. Bools, for example, are defined to be one byte in
size, but their representation allows only for 0 and 1 values to be used
in said byte -- everything else is undefined behavior. Enums have
similar problems, in that not the entire space of the backing type is
necessarily used by variants.
The result is an unsoundness issue in our generated skeletons, because C
code could accidentally set an enum to an invalid value and Rust code
would exhibit undefined behavior.
This change fixes this problem by wrapping the corresponding attributes
in MaybeUninit. In so doing users have to be explicit when accessing
them and make sure that the current representation is indeed valid.

Closes: #589
Signed-off-by: Daniel Müller <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants