Skip to content

Commit

Permalink
Add support for TLS sockets
Browse files Browse the repository at this point in the history
This adds the module std.net.tls and refactors std.net.socket in various
places, such that we can provide support for TLS 1.2 and TLS 1.3.

The TLS stack is backed by Rustls (https://github.com/rustls/rustls). My
original plan was to write the stack in Inko, but I deemed this far too
time consuming and not beneficial for users (compared to using an
existing mature stack). I also experimented with OpenSSL, but using
OpenSSL is like walking through a minefield, and its API is a pain to
use (in part due to its use of global and thread-local state).

Rustls is compiled such that it uses the "ring" backend instead of
aws-lc. This is done because aws-lc requires additional dependencies on
FreeBSD, and increases compile times significantly (about 30 seconds or
so). While performance of TLS 1.3 is less ideal when using ring compared
to using aws-lc (rustls/rustls#1751), it
should still be good enough (and still be much faster compared to using
OpenSSL).

A downside of using Rustls is that the executable sizes increase by
about 6 MiB (or 2 MiB when stripping them), due to the extra code
introduced by Rustls and its dependencies. Sadly we can't avoid this
unless we use OpenSSL, which introduces far more pressing issues.

For certificate validation we use a patched version of the
rustls-platform-verifier crate. The patched version strips the code we
don't need (mostly so we don't get tons of "this code is unused"
warnings and what not), and patches the macOS code to account for the
system verification process being (potentially) slow by using the
`Process::blocking` method.

This fixes #329.

Changelog: added
  • Loading branch information
yorickpeterse committed Jul 23, 2024
1 parent 915b00c commit 8e70fd6
Show file tree
Hide file tree
Showing 44 changed files with 3,384 additions and 795 deletions.
309 changes: 215 additions & 94 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion compiler/src/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,13 @@ pub(crate) fn link(
cmd.arg("-lm");
cmd.arg("-lpthread");
}
_ => {}
OperatingSystem::Mac => {
// This is needed for TLS support.
for name in ["Security", "CoreFoundation"] {
cmd.arg("-framework");
cmd.arg(name);
}
}
}

let mut static_linking = state.config.static_linking;
Expand Down
29 changes: 29 additions & 0 deletions rt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,35 @@ unicode-segmentation = "^1.10"
backtrace = "^0.3"
rustix = { version = "^0.38", features = ["fs", "mm", "param", "process", "net", "std", "time", "event"], default-features = false }

# The dependencies needed for TLS support.
#
# We use ring instead of the default aws-lc-sys because:
#
# 1. aws-lc-sys requires cmake to be installed when building on FreeBSD (and
# potentially other platforms), as aws-lc-sys only provides generated
# bindings for a limited set of platforms
# 2. aws-lc-sys increases compile times quite a bit
# 3. We don't care about FIPS compliance at the time of writing
rustls = { version = "^0.23", features = ["ring", "tls12", "std"], default-features = false }
rustls-pemfile = "^2.1"

# These dependencies are used by the customized version of
# rustls-platform-modifier. We include a custom version so we can deal with the
# platform verification process being potentially slow. See
# https://github.com/rustls/rustls/issues/850 and
# https://github.com/inko-lang/inko/issues/329 for more details.
once_cell = "1.9"

[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "tvos")))'.dependencies]
rustls-native-certs = "0.7"
webpki = { package = "rustls-webpki", version = "0.102", default-features = false }

[target.'cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos"))'.dependencies]
core-foundation = "0.9"
core-foundation-sys = "0.8"
security-framework = { version = "2.10", features = ["OSX_10_14"] }
security-framework-sys = { version = "2.10", features = ["OSX_10_14"] }

[dependencies.socket2]
version = "^0.5"
features = ["all"]
29 changes: 15 additions & 14 deletions rt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@
#![allow(clippy::missing_safety_doc)]
#![allow(clippy::too_many_arguments)]

pub mod macros;
mod macros;

pub mod arc_without_weak;
pub mod config;
pub mod context;
pub mod mem;
pub mod memory_map;
pub mod network_poller;
pub mod process;
pub mod result;
pub mod runtime;
pub mod scheduler;
pub mod socket;
pub mod stack;
pub mod state;
mod arc_without_weak;
mod config;
mod context;
mod mem;
mod memory_map;
mod network_poller;
mod process;
mod result;
mod runtime;
mod rustls_platform_verifier;
mod scheduler;
mod socket;
mod stack;
mod state;

#[cfg(test)]
pub mod test;
1 change: 1 addition & 0 deletions rt/src/network_poller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const CAPACITY: usize = 1024;
pub(crate) type NetworkPoller = sys::Poller;

/// The type of event a poller should wait for.
#[derive(Debug)]
pub(crate) enum Interest {
Read,
Write,
Expand Down
20 changes: 11 additions & 9 deletions rt/src/network_poller/kqueue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,20 @@ impl Poller {
source: impl AsFd,
interest: Interest,
) {
let fd = source.as_fd().as_raw_fd();
let (add, del) = match interest {
Interest::Read => (EventFilter::Read(fd), EventFilter::Write(fd)),
Interest::Write => (EventFilter::Write(fd), EventFilter::Read(fd)),
};
let id = process.identifier() as isize;
let fd = source.as_fd().as_raw_fd();
let flags =
EventFlags::CLEAR | EventFlags::ONESHOT | EventFlags::RECEIPT;
let events = [
Event::new(add, EventFlags::ADD | flags, id),
Event::new(del, EventFlags::DELETE, 0),
];
let events = match interest {
Interest::Read => [
Event::new(EventFilter::Read(fd), EventFlags::ADD | flags, id),
Event::new(EventFilter::Write(fd), EventFlags::DELETE, 0),
],
Interest::Write => [
Event::new(EventFilter::Write(fd), EventFlags::ADD | flags, id),
Event::new(EventFilter::Read(fd), EventFlags::DELETE, 0),
],
};

self.apply(&events);
}
Expand Down
1 change: 0 additions & 1 deletion rt/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use crate::scheduler::process::Thread;
use crate::scheduler::timeouts::Timeout;
use crate::stack::Stack;
use crate::state::State;
use backtrace;
use std::alloc::{alloc, dealloc, handle_alloc_error, Layout};
use std::cell::UnsafeCell;
use std::collections::VecDeque;
Expand Down
8 changes: 6 additions & 2 deletions rt/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ pub(crate) fn error_to_int(error: io::Error) -> i64 {
// raw_os_error() above returns a None.
Errno::TIMEDOUT.raw_os_error()
} else {
-1
match error.kind() {
io::ErrorKind::InvalidData => -2,
io::ErrorKind::UnexpectedEof => -3,
_ => -1,
}
};

code as i64
Expand Down Expand Up @@ -60,7 +64,7 @@ impl Result {
}

pub(crate) fn io_error(error: io::Error) -> Result {
Self::error({ error_to_int(error) } as _)
Self::error(error_to_int(error) as _)
}
}

Expand Down
7 changes: 7 additions & 0 deletions rt/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod stdio;
mod string;
mod sys;
mod time;
mod tls;

use crate::config::Config;
use crate::mem::ClassPointer;
Expand Down Expand Up @@ -67,6 +68,12 @@ pub unsafe extern "system" fn inko_runtime_new(
// does for us when compiling an executable.
signal_sched::block_all();

// Configure the TLS provider. This must be done once before we start the
// program.
rustls::crypto::ring::default_provider()
.install_default()
.expect("failed to set up the default TLS cryptography provider");

Box::into_raw(Box::new(Runtime::new(&*counts, args)))
}

Expand Down
20 changes: 0 additions & 20 deletions rt/src/runtime/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,6 @@ pub unsafe extern "system" fn inko_env_size(state: *const State) -> i64 {
(*state).environment.len() as _
}

#[no_mangle]
pub unsafe extern "system" fn inko_env_home_directory(
state: *const State,
) -> InkoResult {
let state = &*state;

// Rather than performing all sorts of magical incantations to get the home
// directory, we're just going to require that HOME is set.
//
// If the home is explicitly set to an empty string we still ignore it,
// because there's no scenario in which Some("") is useful.
state
.environment
.get("HOME")
.filter(|&path| !path.is_empty())
.cloned()
.map(|v| InkoResult::ok(InkoString::alloc(state.string_class, v) as _))
.unwrap_or_else(InkoResult::none)
}

#[no_mangle]
pub unsafe extern "system" fn inko_env_temp_directory(
state: *const State,
Expand Down
51 changes: 51 additions & 0 deletions rt/src/runtime/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
use crate::context;
use crate::network_poller::Interest;
use crate::process::ProcessPointer;
use crate::scheduler::timeouts::Timeout;
use crate::socket::Socket;
use crate::state::State;
use std::io::{self, Read};

/// Reads a number of bytes from a buffer into a Vec.
Expand All @@ -14,3 +20,48 @@ pub(crate) fn read_into<T: Read>(

Ok(read as i64)
}

pub(crate) fn poll(
state: &State,
mut process: ProcessPointer,
socket: &mut Socket,
interest: Interest,
deadline: i64,
) -> io::Result<()> {
let poll_id = unsafe { process.thread() }.network_poller;

// We must keep the process' state lock open until everything is registered,
// otherwise a timeout thread may reschedule the process (i.e. the timeout
// is very short) before we finish registering the socket with a poller.
{
let mut proc_state = process.state();

// A deadline of -1 signals that we should wait indefinitely.
if deadline >= 0 {
let time = Timeout::until(deadline as u64);

proc_state.waiting_for_io(Some(time.clone()));
state.timeout_worker.suspend(process, time);
} else {
proc_state.waiting_for_io(None);
}

socket.register(state, process, poll_id, interest);
}

// Safety: the current thread is holding on to the process' run lock, so if
// the process gets rescheduled onto a different thread, said thread won't
// be able to use it until we finish this context switch.
unsafe { context::switch(process) };

if process.timeout_expired() {
// The socket is still registered at this point, so we have to
// deregister first. If we don't and suspend for another IO operation,
// the poller could end up rescheduling the process multiple times (as
// there are multiple events still in flight for the process).
socket.deregister(state);
return Err(io::Error::from(io::ErrorKind::TimedOut));
}

Ok(())
}
Loading

0 comments on commit 8e70fd6

Please sign in to comment.