comparison src/libpam/memory.rs @ 93:efc2b56c8928

Remove undefined behavior per MIRI. This replaces a bunch of raw pointers with NonNull and removes all the undefined behavior that we can find with MIRI. We also remove the `SecureString` dependency (since it doesn't work with MIRI, and because it's not really necessary).
author Paul Fisher <paul@pfish.zone>
date Mon, 23 Jun 2025 13:02:58 -0400
parents 5aa1a010f1e8
children 51c9d7e8261a
comparison
equal deleted inserted replaced
92:5ddbcada30f2 93:efc2b56c8928
2 2
3 use crate::Result; 3 use crate::Result;
4 use crate::{BinaryData, ErrorCode}; 4 use crate::{BinaryData, ErrorCode};
5 use std::ffi::{c_char, CStr, CString}; 5 use std::ffi::{c_char, CStr, CString};
6 use std::marker::{PhantomData, PhantomPinned}; 6 use std::marker::{PhantomData, PhantomPinned};
7 use std::{ptr, slice}; 7 use std::mem::offset_of;
8 use std::ptr::NonNull;
9 use std::{mem, ptr, slice};
8 10
9 /// Allocates `count` elements to hold `T`. 11 /// Allocates `count` elements to hold `T`.
10 #[inline] 12 #[inline]
11 pub fn calloc<T>(count: usize) -> *mut T { 13 pub fn calloc<T>(count: usize) -> *mut T {
12 // SAFETY: it's always safe to allocate! Leaking memory is fun! 14 // SAFETY: it's always safe to allocate! Leaking memory is fun!
48 /// <code>pam_get_<var>whatever</var></code> function. 50 /// <code>pam_get_<var>whatever</var></code> function.
49 /// 51 ///
50 /// # Safety 52 /// # Safety
51 /// 53 ///
52 /// It's on you to provide a valid string. 54 /// It's on you to provide a valid string.
53 pub unsafe fn copy_pam_string(result_ptr: *const libc::c_char) -> Result<String> { 55 pub unsafe fn copy_pam_string(result_ptr: *const c_char) -> Result<String> {
54 // We really shouldn't get a null pointer back here, but if we do, return nothing. 56 Ok(wrap_string(result_ptr)?
55 if result_ptr.is_null() {
56 return Ok(String::new());
57 }
58 let bytes = unsafe { CStr::from_ptr(result_ptr) };
59 bytes
60 .to_str()
61 .map(String::from) 57 .map(String::from)
62 .map_err(|_| ErrorCode::ConversationError) 58 .unwrap_or_default())
63 } 59 }
64 60
65 /// Wraps a string returned from PAM as an `Option<&str>`. 61 /// Wraps a string returned from PAM as an `Option<&str>`.
66 pub unsafe fn wrap_string<'a>(data: *const libc::c_char) -> Result<Option<&'a str>> { 62 pub unsafe fn wrap_string<'a>(data: *const c_char) -> Result<Option<&'a str>> {
67 let ret = if data.is_null() { 63 match NonNull::new(data.cast_mut()) {
68 None 64 Some(data) => Ok(Some(
69 } else { 65 CStr::from_ptr(data.as_ptr())
70 Some(
71 CStr::from_ptr(data)
72 .to_str() 66 .to_str()
73 .map_err(|_| ErrorCode::ConversationError)?, 67 .map_err(|_| ErrorCode::ConversationError)?,
74 ) 68 )),
75 }; 69 None => Ok(None),
76 Ok(ret) 70 }
77 } 71 }
78 72
79 /// Allocates a string with the given contents on the C heap. 73 /// Allocates a string with the given contents on the C heap.
80 /// 74 ///
81 /// This is like [`CString::new`], but: 75 /// This is like [`CString::new`], but:
82 /// 76 ///
83 /// - it allocates data on the C heap with [`libc::malloc`]. 77 /// - it allocates data on the C heap with [`libc::malloc`].
84 /// - it doesn't take ownership of the data passed in. 78 /// - it doesn't take ownership of the data passed in.
85 pub fn malloc_str(text: &str) -> Result<*mut c_char> { 79 pub fn malloc_str(text: &str) -> Result<NonNull<c_char>> {
86 let data = text.as_bytes(); 80 let data = text.as_bytes();
87 if data.contains(&0) { 81 if data.contains(&0) {
88 return Err(ErrorCode::ConversationError); 82 return Err(ErrorCode::ConversationError);
89 } 83 }
90 // +1 for the null terminator 84 // +1 for the null terminator
91 let data_alloc: *mut c_char = calloc(data.len() + 1); 85 let data_alloc: *mut c_char = calloc(data.len() + 1);
92 // SAFETY: we just allocated this and we have enough room. 86 // SAFETY: we just allocated this and we have enough room.
93 unsafe { 87 unsafe {
94 libc::memcpy(data_alloc.cast(), data.as_ptr().cast(), data.len()); 88 libc::memcpy(data_alloc.cast(), data.as_ptr().cast(), data.len());
95 } 89 Ok(NonNull::new_unchecked(data_alloc))
96 Ok(data_alloc) 90 }
97 } 91 }
98 92
99 /// Writes zeroes over the contents of a C string. 93 /// Writes zeroes over the contents of a C string.
100 /// 94 ///
101 /// This won't overwrite a null pointer. 95 /// This won't overwrite a null pointer.
103 /// # Safety 97 /// # Safety
104 /// 98 ///
105 /// It's up to you to provide a valid C string. 99 /// It's up to you to provide a valid C string.
106 pub unsafe fn zero_c_string(cstr: *mut c_char) { 100 pub unsafe fn zero_c_string(cstr: *mut c_char) {
107 if !cstr.is_null() { 101 if !cstr.is_null() {
108 libc::memset(cstr.cast(), 0, libc::strlen(cstr.cast())); 102 let len = libc::strlen(cstr.cast());
103 for x in 0..len {
104 ptr::write_volatile(cstr.byte_offset(x as isize), mem::zeroed())
105 }
109 } 106 }
110 } 107 }
111 108
112 /// Binary data used in requests and responses. 109 /// Binary data used in requests and responses.
113 /// 110 ///
126 _marker: Immovable, 123 _marker: Immovable,
127 } 124 }
128 125
129 impl CBinaryData { 126 impl CBinaryData {
130 /// Copies the given data to a new BinaryData on the heap. 127 /// Copies the given data to a new BinaryData on the heap.
131 pub fn alloc((data, data_type): (&[u8], u8)) -> Result<*mut CBinaryData> { 128 pub fn alloc((data, data_type): (&[u8], u8)) -> Result<NonNull<CBinaryData>> {
132 let buffer_size = 129 let buffer_size =
133 u32::try_from(data.len() + 5).map_err(|_| ErrorCode::ConversationError)?; 130 u32::try_from(data.len() + 5).map_err(|_| ErrorCode::ConversationError)?;
134 // SAFETY: We're only allocating here. 131 // SAFETY: We're only allocating here.
135 let dest = unsafe { 132 let dest = unsafe {
136 let dest_buffer: *mut CBinaryData = calloc::<u8>(buffer_size as usize).cast(); 133 let mut dest_buffer: NonNull<Self> =
137 let dest = &mut *dest_buffer; 134 NonNull::new_unchecked(calloc::<u8>(buffer_size as usize).cast());
135 let dest = dest_buffer.as_mut();
138 dest.total_length = buffer_size.to_be_bytes(); 136 dest.total_length = buffer_size.to_be_bytes();
139 dest.data_type = data_type; 137 dest.data_type = data_type;
140 let dest = dest.data.as_mut_ptr(); 138 libc::memcpy(
141 libc::memcpy(dest.cast(), data.as_ptr().cast(), data.len()); 139 Self::data_ptr(dest_buffer).cast(),
140 data.as_ptr().cast(),
141 data.len(),
142 );
142 dest_buffer 143 dest_buffer
143 }; 144 };
144 Ok(dest) 145 Ok(dest)
145 } 146 }
146 147
147 fn length(&self) -> usize { 148 fn length(&self) -> usize {
148 u32::from_be_bytes(self.total_length).saturating_sub(5) as usize 149 u32::from_be_bytes(self.total_length).saturating_sub(5) as usize
149 } 150 }
150 151
151 /// Clears this data and frees it. 152 fn data_ptr(ptr: NonNull<Self>) -> *mut u8 {
152 pub unsafe fn zero_contents(&mut self) { 153 unsafe {
153 let contents = slice::from_raw_parts_mut(self.data.as_mut_ptr(), self.length()); 154 ptr.as_ptr()
154 for v in contents { 155 .cast::<u8>()
155 *v = 0 156 .byte_offset(offset_of!(Self, data) as isize)
156 } 157 }
157 self.data_type = 0; 158 }
158 self.total_length = [0; 4]; 159
159 } 160 unsafe fn data_slice<'a>(ptr: NonNull<Self>) -> &'a mut [u8] {
160 } 161 unsafe { slice::from_raw_parts_mut(Self::data_ptr(ptr), ptr.as_ref().length()) }
161 162 }
162 impl<'a> From<&'a CBinaryData> for (&'a [u8], u8) { 163
163 fn from(value: &'a CBinaryData) -> Self { 164 pub unsafe fn data<'a>(ptr: NonNull<Self>) -> (&'a [u8], u8) {
164 ( 165 unsafe { (Self::data_slice(ptr), ptr.as_ref().data_type) }
165 unsafe { slice::from_raw_parts(value.data.as_ptr(), value.length()) }, 166 }
166 value.data_type, 167
167 ) 168 pub unsafe fn zero_contents(ptr: NonNull<Self>) {
168 } 169 for byte in Self::data_slice(ptr) {
169 } 170 ptr::write_volatile(byte as *mut u8, mem::zeroed());
170 171 }
171 impl From<&'_ CBinaryData> for BinaryData { 172 ptr::write_volatile(ptr.as_ptr(), mem::zeroed());
172 fn from(value: &'_ CBinaryData) -> Self { 173 }
173 // This is a dumb trick but I like it because it is simply the presence 174
174 // of `.map(|z: (_, _)| z)` in the middle of this that gives 175 #[allow(clippy::wrong_self_convention)]
175 // type inference the hint it needs to make this work. 176 pub unsafe fn as_binary_data(ptr: NonNull<Self>) -> BinaryData {
176 let [ret] = [value].map(Into::into).map(|z: (_, _)| z).map(Into::into); 177 let (data, data_type) = unsafe { (CBinaryData::data_slice(ptr), ptr.as_ref().data_type) };
177 ret 178 (Vec::from(data), data_type).into()
178 }
179 }
180
181 impl From<Option<&'_ CBinaryData>> for BinaryData {
182 fn from(value: Option<&CBinaryData>) -> Self {
183 value.map(Into::into).unwrap_or_default()
184 } 179 }
185 } 180 }
186 181
187 #[cfg(test)] 182 #[cfg(test)]
188 mod tests { 183 mod tests {
191 ErrorCode, 186 ErrorCode,
192 }; 187 };
193 #[test] 188 #[test]
194 fn test_strings() { 189 fn test_strings() {
195 let str = malloc_str("hello there").unwrap(); 190 let str = malloc_str("hello there").unwrap();
191 let str = str.as_ptr();
196 malloc_str("hell\0 there").unwrap_err(); 192 malloc_str("hell\0 there").unwrap_err();
197 unsafe { 193 unsafe {
198 let copied = copy_pam_string(str).unwrap(); 194 let copied = copy_pam_string(str).unwrap();
199 assert_eq!("hello there", copied); 195 assert_eq!("hello there", copied);
200 zero_c_string(str); 196 zero_c_string(str);