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

Add readv and writev syscalls for SGX #369

Merged
merged 5 commits into from Apr 3, 2020
Merged

Conversation

lkatalin
Copy link
Contributor

@lkatalin lkatalin commented Mar 26, 2020

This is a draft. In particular, it needs some help with casting pointers in a safe and correct way.

Resolves #375 and resolves #376

@lkatalin lkatalin self-assigned this Mar 26, 2020
@lkatalin lkatalin added the intel sgx Issues related to Intel SGX label Mar 26, 2020
enarx-keep-sgx-shim/src/handler.rs Outdated Show resolved Hide resolved
Comment on lines 199 to 203
let iovec = unsafe { from_raw_parts_mut(iovec as *mut Iovec, iovcnt) }; // ???
let mut bufsize: usize = 0;
for i in 0..iovcnt {
bufsize += iovec[i].size;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let iovec = unsafe { from_raw_parts_mut(iovec as *mut Iovec, iovcnt) }; // ???
let mut bufsize: usize = 0;
for i in 0..iovcnt {
bufsize += iovec[i].size;
}
let bufsize = trusted.iter().fold(0, |a, e| a + e.size);

}

// Allocate some unencrypted memory.
let size = bufsize + size_of::<Iovec>() * iovcnt;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let size = bufsize + size_of::<Iovec>() * iovcnt;
let size = bufsize + size_of::<Iovec>() * trusted.len();

};

// Cast beginning of allocated memory as iovec slice of length iovcnt.
let map = unsafe { from_raw_parts_mut(map as *mut Iovec, iovcnt) };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let map = unsafe { from_raw_parts_mut(map as *mut Iovec, iovcnt) };
let untrusted = unsafe { from_raw_parts_mut(map as *mut Iovec, iovcnt) };

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general strategy with my naming here is to always communicate to future readers whether the types are trusted or not. We want to avoid accidents...

let map = unsafe { from_raw_parts_mut(map as *mut Iovec, iovcnt) };

// Set pointers in unencrypted iovec slice to use the rest of the allocated memory.
let mut chunk = map.as_ptr() as usize + size_of::<Iovec>() * iovcnt; // ??? May be unsafe or illegal as per from_raw_parts_mut rules
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the question here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of whether I'm allowed to do map.as_ptr() due to: "The memory referenced by the returned slice must not be accessed through any other pointer (not derived from the return value) for the duration of lifetime 'a. Both read and write accesses are forbidden." from https://doc.rust-lang.org/core/slice/fn.from_raw_parts_mut.html. But since the pointer is derived from the original 'map', maybe it's okay.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lkatalin What it means in this case is don't access the memory in this region. You can still use the base pointer to calculate a new pointer to memory after the region without harm.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@npmccallum Ah okay, thanks as usual for the insights.


// Set pointers in unencrypted iovec slice to use the rest of the allocated memory.
let mut chunk = map.as_ptr() as usize + size_of::<Iovec>() * iovcnt; // ??? May be unsafe or illegal as per from_raw_parts_mut rules
for i in 0..iovcnt {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for i in 0..iovcnt {
for (t, u) in trusted.iter().zip(untrusted.iter()) {

Comment on lines 223 to 224
map[i].base = chunk as *mut u8;
chunk = chunk + iovec[i].size;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
map[i].base = chunk as *mut u8;
chunk = chunk + iovec[i].size;
u.base = chunk;
u.size = t.size;
chunk = chunk.add(t.size);

0,
0,
); // ???
self.ufree(map.as_ptr() as *mut u8, size as u64); // ???
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't free this until after you are done copying it...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean after copy_nonoverlapping, right? I was confused about this because in read() the ufree is called immediately after the syscall, which I didn't understand.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lkatalin You probably don't understand it because it probably doesn't work. I was a bad boy and didn't test read() obviously... :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough!

Comment on lines 246 to 290
for i in 0..iovcnt {
core::ptr::copy_nonoverlapping(
map[i].base as *mut u8,
iovec[i].base as *mut u8,
ret as _,
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iterate over the slice rather than using an index. However, keep in mind that we handed the whole array of iovecs to the untrusted host. That means that we need to validate that the values haven't been manipulated. For example, the host might replace iovec[0].base with a pointer to enclave memory in order to trick us into copying encrypted memory somewhere else. So, like we did when initializing the array, we need to iterate over both slices. Validate that the untrusted iovec hasn't been modified (and trigger attacked() if it has; that is suspicious behavior). Then do the copy.

Alternatively, we could take advantage of the contiguousness of the map buffer to bypass the untrusted array altogether. This would probably be fewer instructions. However, we'd bypass an opportunity to detect malicious behavior. So I'd prefer to be slightly slower and more security conscious.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, the host might replace iovec[0].base with a pointer to enclave memory ... Validate that the untrusted iovec hasn't been modified

@npmccallum For now, I'm assuming this validation means to check that none of the base pointers refer to something inside enclave memory. If there are other checks that should be done here, please let me know. I'll also continue to think on it.

I'm fine with doing the slower and more security-conscious version.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing that the base pointers (and base+size pointers) aren't inside the enclave is a weaker version of the testing I had in mind. I wanted to recreate the exact expected base and size that it should be and validate that the results haven't been tampered with in any way.

Comment on lines 195 to 196
let iovec = self.aex.gpr.rsi as *mut u8;
let iovcnt = self.aex.gpr.rdx as usize;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let iovec = self.aex.gpr.rsi as *mut u8;
let iovcnt = self.aex.gpr.rdx as usize;
let trusted = unsafe {
from_raw_parts_mut(
self.aex.gpr.rsi as *mut Iovec,
self.aex.gpr.rdx as usize
);
};

@npmccallum
Copy link
Member

@lkatalin Also note that clippy is giving you really good feedback here.

@lkatalin lkatalin force-pushed the readvwritev branch 2 times, most recently from 587f363 to b3b38ff Compare March 27, 2020 17:34
@lkatalin lkatalin changed the title Readvwritev Add readv syscall for SGX Mar 27, 2020
@lkatalin lkatalin marked this pull request as ready for review March 27, 2020 17:50
Copy link
Member

@npmccallum npmccallum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not at all a fan of casting a pointer through an integer to get rid of the alignment warning. All that does it hide the problem.

I think a better strategy here is to convert the map to a byte slice immediately. Then use split_at_mut to divide the slice into the iovec prefix and the buffer suffix. Then use align_to_mut to convert the prefix from a byte slice into an iovec slice.

@lkatalin
Copy link
Contributor Author

@npmccallum Good point; I've tried to do it better here without casting through an integer.

Comment on lines 216 to 255
let (pre, untrusted, suf) = unsafe { uiovec.align_to_mut::<Iovec>() };
if pre != [] || suf != [] {
self.attacked();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let (pre, untrusted, suf) = unsafe { uiovec.align_to_mut::<Iovec>() };
if pre != [] || suf != [] {
self.attacked();
}
let (_, untrusted, _) = unsafe { uiovec.align_to_mut::<Iovec>() };
if untrusted.len() != trusted.len() {
self.attacked();
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does make way more sense as a method of checking the same thing. Thank you.

Comment on lines 222 to 265
let mut chunk = ubuffer.as_ptr();
for (t, mut u) in trusted.iter_mut().zip(untrusted.iter_mut()) {
u.base = chunk as *mut u8;
u.size = t.size;
chunk = unsafe { chunk.add(t.size) };
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of converting the ubuffer to a pointer and then doing pointer math, instead track the offset and convert a subslice to a pointer. That will remove the untrusted code block.

Comment on lines 247 to 290
let mut chunk = ubuffer.as_ptr();
for (t, u) in trusted.iter().zip(untrusted.iter()) {
if u.base != chunk as *mut u8 || u.size != t.size {
self.attacked();
}
core::ptr::copy_nonoverlapping(u.base as *mut u8, t.base as *mut u8, ret as _);
chunk = chunk.add(t.size);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Comment on lines 255 to 259
self.ufree(map.as_ptr() as *mut u8, size as u64);
}
ret
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.ufree(map.as_ptr() as *mut u8, size as u64);
}
ret
}
self.ufree(map.as_ptr() as *mut u8, size as u64);
ret

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also makes lots more sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I can't move this because ret is known only within the unsafe block.

Comment on lines 233 to 241
let ret = self.syscall(
SysCall::READV,
fd,
untrusted.as_ptr() as _,
trusted.len() as u64,
0,
0,
0,
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let ret = self.syscall(
SysCall::READV,
fd,
untrusted.as_ptr() as _,
trusted.len() as u64,
0,
0,
0,
);
let ret = unsafe {
self.syscall(
SysCall::READV,
fd,
untrusted.as_ptr() as _,
trusted.len() as u64,
0,
0,
0,
)
};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That enables you to reduce the size of the unsafe block significantly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! Yes, I should have done this.


let mut offset = 0;
for (t, u) in trusted.iter().zip(untrusted.iter()) {
if u.base != ubuffer[offset] as *mut u8 || u.size != t.size {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cast doesn't do what you think it does. It converts the byte at offset to a *const u8.

if u.base != ubuffer[offset] as *mut u8 || u.size != t.size {
self.attacked();
}
core::ptr::copy_nonoverlapping(u.base as *mut u8, t.base as *mut u8, ret as _);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the slice method copy_from_slice() instead.

Copy link
Contributor Author

@lkatalin lkatalin Mar 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since u.base is a *mut u8, would I not have to turn it into a slice with the unsafe core::slice::from_raw_parts_mut in order to use that method? That would result in the same number unsafe lines here (1). Is there a better way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know the offset into the ubuffer, which is already a slice.

let bufsize = trusted.iter().fold(0, |a, e| a + e.size);

// Allocate some unencrypted memory.
let size = bufsize + size_of::<Iovec>() * trusted.len();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefix = sizeof * len

SysCall::READV,
fd,
untrusted.as_ptr() as _,
trusted.len() as u64,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
trusted.len() as u64,
untrusted.len() as u64,

These values are the same. But I think this is slightly more readable.

enarx-keep-sgx-shim/src/handler.rs Outdated Show resolved Hide resolved
enarx-keep-sgx-shim/src/handler.rs Show resolved Hide resolved
base: *mut u8,
/// Number of bytes to transfer
size: usize,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This struct probably belongs in nolibc.

@lkatalin lkatalin force-pushed the readvwritev branch 4 times, most recently from 7a5cee7 to f46ddc3 Compare March 31, 2020 22:01
@lkatalin lkatalin changed the title Add readv syscall for SGX Add readv and writev syscalls for SGX Mar 31, 2020
@lkatalin lkatalin force-pushed the readvwritev branch 3 times, most recently from 054b3e2 to dcecb58 Compare March 31, 2020 22:56
@lkatalin
Copy link
Contributor Author

@npmccallum I put the Iovec into nolibc. I hope I used the lifetimes correctly. There is probably some room for improvement there.

I also went ahead and included the writev call as well.

I am pretty sure since a Rust slice is a wrapper for a pointer that the way I copy them after converting to a slice from Iovec should work to update the memory pointed to without any further assignments, but it is worth double checking (ex. ln 308).

@lkatalin lkatalin linked an issue Apr 1, 2020 that may be closed by this pull request
Comment on lines 45 to 51
impl<'a, T> Iovec<'a, T> {
pub fn as_slice(&self) -> &[T] {
let slice: &[T] = self.into();
slice
}

pub fn as_mut_slice(&mut self) -> &mut [T] {
let slice: &mut [T] = self.into();
slice
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

impl AsRef<[u8]> and impl AsMut<[u8]>

Comment on lines 21 to 39
/// Buffer used by readv() and writev() syscalls
#[repr(C)]
pub struct Iovec<'a, T: 'a> {
/// Buffer start address
pub base: *mut T,

/// Number of bytes to transfer
pub size: usize,

phantom: core::marker::PhantomData<&'a T>,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Buffer used by readv() and writev() syscalls
#[repr(C)]
pub struct Iovec<'a, T: 'a> {
/// Buffer start address
pub base: *mut T,
/// Number of bytes to transfer
pub size: usize,
phantom: core::marker::PhantomData<&'a T>,
}
/// Buffer used by readv() and writev() syscalls
#[repr(C)]
pub struct Iovec<'a> {
/// Buffer start address
pub base: *mut u8,
/// Number of bytes to transfer
pub size: usize,
phantom: core::marker::PhantomData<&'a T>,
}

That doesn't do what you think it does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to learn more about this, obviously. I thought it would tie the lifetime to the base. For now, Rust won't let me compile it without the T in the struct parameters: pub struct Iovec<'a, T> since it's required in PhantomData. I changed the T in the base to a u8.

Comment on lines 294 to 340
let mut offset = 0;
for (t, mut u) in trusted.iter_mut().zip(untrusted.iter_mut()) {
u.base = ubuffer[offset..].as_mut_ptr();
u.size = t.size;
offset += t.size;
}

// Copy the encrypted input into unencrypted memory.
let mut offset = 0;
for (t, u) in trusted.iter_mut().zip(untrusted.iter_mut()) {
if u.base != ubuffer[offset..].as_mut_ptr() || u.size != t.size {
self.attacked();
}

u.as_mut_slice().copy_from_slice(t.as_mut_slice());
offset += t.size;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need two loops in this case. Also, we can drop the attacked check.

self.attacked()
}

unsafe { self.ufree(map.as_ptr() as *mut u8, size as u64) };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to immediately after the syscall.

Copy link
Member

@npmccallum npmccallum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mostly looks good. We're almost there!

Comment on lines 22 to 39
#[repr(C)]
pub struct Iovec<'a, T> {
/// Buffer start address
pub base: *mut u8,

/// Number of bytes to transfer
pub size: usize,

phantom: core::marker::PhantomData<&'a T>,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[repr(C)]
pub struct Iovec<'a, T> {
/// Buffer start address
pub base: *mut u8,
/// Number of bytes to transfer
pub size: usize,
phantom: core::marker::PhantomData<&'a T>,
}
#[repr(C)]
pub struct Iovec<'a> {
/// Buffer start address
pub base: *mut u8,
/// Number of bytes to transfer
pub size: usize,
phantom: core::marker::PhantomData<&'a ()>,
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize one could do this. Seems much better!

self.attacked();
}

t.as_mut().copy_from_slice(u.as_mut());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
t.as_mut().copy_from_slice(u.as_mut());
t.as_mut().copy_from_slice(u.as_ref());

Comment on lines 291 to 294
// Set pointers in unencrypted iovec slice to use the rest of the allocated memory,
// then copy the encrypted input into unencrypted memory.
// The offset is into the buffer area allocated immediately after the iovec struct
// array, measured in bytes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Properly wrap the paragraph.

for (t, mut u) in trusted.iter_mut().zip(untrusted.iter_mut()) {
u.base = ubuffer[offset..].as_mut_ptr();
u.size = t.size;
u.as_mut().copy_from_slice(t.as_mut());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
u.as_mut().copy_from_slice(t.as_mut());
u.as_mut().copy_from_slice(t.as_ref());

@lkatalin lkatalin force-pushed the readvwritev branch 2 times, most recently from 5131e47 to 9b839f2 Compare April 2, 2020 23:47
@npmccallum npmccallum merged commit 66c452c into enarx:master Apr 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
intel sgx Issues related to Intel SGX
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add readv, writev syscalls for SGX Add more security checks in ualloc()
2 participants