This commit is contained in:
dr-frmr 2023-10-05 15:03:42 -04:00
parent 1620538e46
commit f5257ae40d
No known key found for this signature in database
20 changed files with 592 additions and 58 deletions

BIN
modules/chess/package.zip Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,19 @@
[
{
"process_id": "chess",
"process_wasm": "chess.wasm",
"on_panic": {
"on_panic": "Restart"
},
"networking": true,
"request_messaging": [
"http_bindings",
"encryptor"
],
"grant_messaging": [
"terminal",
"http_bindings",
"encryptor"
]
}
]

Binary file not shown.

View File

@ -0,0 +1,14 @@
[
{
"process_id": "terminal",
"process_wasm": "terminal.wasm",
"on_panic": {
"on_panic": "Restart"
},
"networking": true,
"request_messaging": [
"net"
],
"grant_messaging": "all"
}
]

View File

@ -0,0 +1 @@
hello

Binary file not shown.

View File

@ -81,12 +81,9 @@ impl Guest for Component {
assert_eq!(our.process, ProcessId::Name("terminal".into())); assert_eq!(our.process, ProcessId::Name("terminal".into()));
print_to_terminal(1, &format!("terminal: start")); print_to_terminal(1, &format!("terminal: start"));
loop { loop {
let message = match receive() { let (source, message) = match receive() {
Ok((source, message)) => { Ok((source, message)) => {
if our.node != source.node { (source, message)
continue;
}
message
} }
Err((error, _context)) => { Err((error, _context)) => {
print_to_terminal(0, &format!("net error: {:?}!", error.kind)); print_to_terminal(0, &format!("net error: {:?}!", error.kind));
@ -99,6 +96,10 @@ impl Guest for Component {
ipc, ipc,
.. ..
}) => { }) => {
if our.node != source.node
|| our.process != source.process {
continue;
}
let Some(command) = ipc else { let Some(command) = ipc else {
continue; continue;
}; };

34
src/bootstrapping.md Normal file
View File

@ -0,0 +1,34 @@
Bootstrapping the kernel
1. We have a bunch of packages that are built and zipped into "packages"
2. On startup, if we don't yet have a filesystem, we grab these packages by name from unix
3. For each package, "unzip" it and read the "manifest" file
4. For each entry in the manifest, start the named process by sending a message to kernel.
```
package.zip
key_value.wasm
key_value_worker.wasm
index.html
my_directory
cool_image.png
.manifest
```
inside .manifest:
(describes processes to start on-install)
```
[
{
"process_id": "key_value",
"process_wasm": "key_value.wasm",
"on_panic": {"on_panic": true},
"networking": true,
"messaging": ["vfs", "http_server", "http_bindings"]
}
]
```

View File

@ -15,6 +15,7 @@ pub async fn load_fs(
home_directory_path: String, home_directory_path: String,
file_key: Vec<u8>, file_key: Vec<u8>,
fs_config: FsConfig, fs_config: FsConfig,
vfs_message_sender: MessageSender,
) -> Result<(ProcessMap, Manifest), FsError> { ) -> Result<(ProcessMap, Manifest), FsError> {
// load/create fs directory, manifest + log if none. // load/create fs directory, manifest + log if none.
let fs_directory_path_str = format!("{}/fs", &home_directory_path); let fs_directory_path_str = format!("{}/fs", &home_directory_path);
@ -80,6 +81,7 @@ pub async fn load_fs(
&kernel_process_id, &kernel_process_id,
&mut process_map, &mut process_map,
&mut manifest, &mut manifest,
&vfs_message_sender,
) )
.await .await
.expect("fresh bootstrap failed!"); .expect("fresh bootstrap failed!");
@ -88,56 +90,52 @@ pub async fn load_fs(
Ok((process_map, manifest)) Ok((process_map, manifest))
} }
// function run only upon fresh boot. /// function run only upon fresh boot.
// goes through /modules, gets their .wasm bytes, injects into fs and kernel state. ///
/// for each folder in /modules, looks for a package.zip file, extracts the contents,
/// sends the contents to VFS, and reads the manifest.json.
///
/// the manifest.json contains instructions for which processes to boot and what
/// capabilities to give them. since we are inside runtime, can spawn those out of
/// thin air.
async fn bootstrap( async fn bootstrap(
our_name: &str, our_name: &str,
kernel_process_id: &FileIdentifier, kernel_process_id: &FileIdentifier,
process_map: &mut ProcessMap, process_map: &mut ProcessMap,
manifest: &mut Manifest, manifest: &mut Manifest,
vfs_message_sender: &MessageSender,
) -> Result<()> { ) -> Result<()> {
let names_and_bytes = get_processes_from_directories().await; let packages: Vec<zip::ZipArchive<std::io::Cursor<Vec<u8>>>> = get_zipped_packages().await;
const RUNTIME_MODULES: [&str; 8] = [
"filesystem",
"http_server",
"http_client",
"encryptor",
"net",
"vfs",
"kernel",
"eth_rpc",
];
let mut special_capabilities: HashSet<Capability> = HashSet::new(); for package in packages {
for (process_name, _) in &names_and_bytes { // for each file in package.zip, recursively through all dirs, send a newfile KM to VFS
special_capabilities.insert(Capability { let mut stack = Vec::new();
issuer: Address { stack.push(package);
node: our_name.to_string(),
process: ProcessId::Name(process_name.into()),
},
params: "\"messaging\"".into(),
});
}
for runtime_module in RUNTIME_MODULES {
special_capabilities.insert(Capability {
issuer: Address {
node: our_name.to_string(),
process: ProcessId::Name(runtime_module.into()),
},
params: "\"messaging\"".into(),
});
}
// give all distro processes the ability to send messages across the network
special_capabilities.insert(Capability {
issuer: Address {
node: our_name.to_string(),
process: ProcessId::Name("kernel".into()),
},
params: "\"network\"".into(),
});
// for a module in /modules, put its bytes into filesystem, add to process_map while let Some(mut package) = stack.pop() {
for (process_name, wasm_bytes) in names_and_bytes { for i in 0..package.len() {
let mut file = package.by_index(i).unwrap();
if file.name().ends_with('/') {
let new_package = zip::ZipArchive::new(std::io::Cursor::new(file.into_inner())).unwrap();
stack.push(new_package);
} else {
let file_path = file.sanitized_name();
let mut file_content = Vec::new();
file.read_to_end(&mut file_content).unwrap();
let km = KernelMessage::NewFile {
path: file_path,
content: file_content,
};
vfs_message_sender.send(km).await.unwrap();
}
}
}
// get and read manifest.json
// for each process-entry in manifest.json:
for entry in process_manifest {
// save in process map
let hash: [u8; 32] = hash_bytes(&wasm_bytes); let hash: [u8; 32] = hash_bytes(&wasm_bytes);
if let Some(id) = manifest.get_uuid_by_hash(&hash).await { if let Some(id) = manifest.get_uuid_by_hash(&hash).await {
@ -169,7 +167,43 @@ async fn bootstrap(
entry.capabilities.extend(special_capabilities.clone()); entry.capabilities.extend(special_capabilities.clone());
entry.wasm_bytes_handle = id; entry.wasm_bytes_handle = id;
} }
// spawn the requested capabilities
// spawn the granted capabilities
} }
}
const RUNTIME_MODULES: [&str; 8] = [
"filesystem",
"http_server",
"http_client",
"encryptor",
"net",
"vfs",
"kernel",
"eth_rpc",
];
let mut runtime_caps: HashSet<Capability> = HashSet::new();
for runtime_module in RUNTIME_MODULES {
runtime_caps.insert(Capability {
issuer: Address {
node: our_name.to_string(),
process: ProcessId::Name(runtime_module.into()),
},
params: "\"messaging\"".into(),
});
}
// give all runtime processes the ability to send messages across the network
runtime_caps.insert(Capability {
issuer: Address {
node: our_name.to_string(),
process: ProcessId::Name("kernel".into()),
},
params: "\"network\"".into(),
});
// finally, save runtime modules in state map as well, somewhat fakely // finally, save runtime modules in state map as well, somewhat fakely
for runtime_module in RUNTIME_MODULES { for runtime_module in RUNTIME_MODULES {
@ -178,9 +212,8 @@ async fn bootstrap(
.or_insert(PersistedProcess { .or_insert(PersistedProcess {
wasm_bytes_handle: 0, wasm_bytes_handle: 0,
on_panic: OnPanic::Restart, on_panic: OnPanic::Restart,
capabilities: HashSet::new(), capabilities: runtime_caps.clone(),
}); });
entry.capabilities.extend(special_capabilities.clone());
} }
// save kernel process state. FsAction::SetState(kernel) // save kernel process state. FsAction::SetState(kernel)
@ -196,6 +229,33 @@ async fn bootstrap(
Ok(()) Ok(())
} }
/// go into /modules folder and get all
async fn get_zipped_packages() -> Vec<zip::ZipArchive<std::io::Cursor<Vec<u8>>>> {
let modules_path = std::path::Path::new("modules");
let mut packages = Vec::new();
if let Ok(mut entries) = fs::read_dir(modules_path).await {
while let Ok(Some(entry)) = entries.next_entry().await {
// get a file named package.zip
if let Some(pkg) = entry.file_name().to_str() {
if pkg == "package.zip" {
// read the file
if let Ok(bytes) = fs::read(entry.path()).await {
// extract the zip
if let Ok(zip) = zip::ZipArchive::new(std::io::Cursor::new(bytes)) {
// add to list of packages
packages.push(zip);
}
}
}
}
}
}
return packages;
}
async fn get_processes_from_directories() -> Vec<(String, Vec<u8>)> { async fn get_processes_from_directories() -> Vec<(String, Vec<u8>)> {
let mut processes = Vec::new(); let mut processes = Vec::new();

View File

@ -192,6 +192,18 @@ impl UqProcessImports for ProcessWasi {
// Ok(()) // Ok(())
} }
async fn get_state(&mut self) -> Result<Option<Vec<u8>>> {
unimplemented!()
}
async fn get_state(&mut self, bytes: Vec<u8>) -> Result<()> {
unimplemented!()
}
async fn clear_state(&mut self) -> Result<()> {
unimplemented!()
}
async fn spawn( async fn spawn(
&mut self, &mut self,
id: wit::ProcessId, id: wit::ProcessId,

View File

@ -378,6 +378,7 @@ async fn main() {
home_directory_path.clone(), home_directory_path.clone(),
file_key, file_key,
fs_config, fs_config,
vfs_message_sender.clone(),
) )
.await .await
.expect("fs load failed!"); .expect("fs load failed!");

View File

@ -0,0 +1,26 @@
// https://github.com/bytecodealliance/wasmtime/blob/432b5471ec4bf6d51173def284cd418be6849a49/crates/wasi/wit/deps/random/insecure-seed.wit
/// The insecure-seed interface for seeding hash-map DoS resistance.
///
/// It is intended to be portable at least between Unix-family platforms and
/// Windows.
interface insecure-seed {
/// Return a 128-bit value that may contain a pseudo-random value.
///
/// The returned value is not required to be computed from a CSPRNG, and may
/// even be entirely deterministic. Host implementations are encouraged to
/// provide pseudo-random values to any program exposed to
/// attacker-controlled content, to enable DoS protection built into many
/// languages' hash-map implementations.
///
/// This function is intended to only be called once, by a source language
/// to initialize Denial Of Service (DoS) protection in its hash-map
/// implementation.
///
/// # Expected future evolution
///
/// This will likely be changed to a value import, to prevent it from being
/// called multiple times and potentially used for purposes other than DoS
/// protection.
insecure-seed: func() -> tuple<u64, u64>
}

View File

@ -0,0 +1,23 @@
// https://github.com/bytecodealliance/wasmtime/blob/432b5471ec4bf6d51173def284cd418be6849a49/crates/wasi/wit/deps/random/insecure.wit
/// The insecure interface for insecure pseudo-random numbers.
///
/// It is intended to be portable at least between Unix-family platforms and
/// Windows.
interface insecure {
/// Return `len` insecure pseudo-random bytes.
///
/// This function is not cryptographically secure. Do not use it for
/// anything related to security.
///
/// There are no requirements on the values of the returned bytes, however
/// implementations are encouraged to return evenly distributed values with
/// a long period.
get-insecure-random-bytes: func(len: u64) -> list<u8>
/// Return an insecure pseudo-random `u64` value.
///
/// This function returns the same type of pseudo-random data as
/// `get-insecure-random-bytes`, represented as a `u64`.
get-insecure-random-u64: func() -> u64
}

View File

@ -0,0 +1,23 @@
// https://github.com/bytecodealliance/wasmtime/blob/432b5471ec4bf6d51173def284cd418be6849a49/crates/wasi/wit/deps/random/random.wit
package wasi:random
interface random {
/// Return `len` cryptographically-secure pseudo-random bytes.
///
/// This function must produce data from an adequately seeded
/// cryptographically-secure pseudo-random number generator (CSPRNG), so it
/// must not block, from the perspective of the calling program, and the
/// returned data is always unpredictable.
///
/// This function must always return fresh pseudo-random data. Deterministic
/// environments must omit this function, rather than implementing it with
/// deterministic data.
get-random-bytes: func(len: u64) -> list<u8>
/// Return a cryptographically-secure pseudo-random `u64` value.
///
/// This function returns the same type of pseudo-random data as
/// `get-random-bytes`, represented as a `u64`.
get-random-u64: func() -> u64
}

183
terminal/uqbar.wit Normal file
View File

@ -0,0 +1,183 @@
package component:uq-process
interface types {
// JSON is passed over WASM boundary as a string.
type json = string
// context is a string of UTF-8 JSON.
// it is used when building a Request to save information
// that will not be part of a Response, in order to more
// easily handle ("contextualize") that Response.
type context = json
variant process-id {
id(u64),
name(string),
}
// TODO better name for this
record address {
node: string,
process: process-id,
}
record payload {
mime: option<string>,
bytes: list<u8>,
}
record request {
// if true, this request inherits context AND payload of incipient
// request, and cannot have its own context.
inherit: bool,
// if Some, this request expects a response in the number of seconds given
expects-response: option<u64>,
ipc: option<json>,
metadata: option<json>,
// to grab payload, use get_payload()
}
record response {
ipc: option<json>,
metadata: option<json>,
// to grab payload, use get_payload()
}
// a message can be a request or a response.
// within a response, there is a result which surfaces any error
// that happened because of a request.
// a successful response will contain the context of the request
// it matches, if any was set.
variant message {
request(request),
response(tuple<response, option<context>>),
}
variant capabilities {
none,
all,
some(list<signed-capability>),
}
record signed-capability {
issuer: address,
params: json,
signature: list<u8>,
}
// network errors come from trying to send a message to another node.
// a message can fail by timing out, or by the node being entirely unreachable (offline).
// in either case, the message is not delivered, and the process that sent it
// receives that message along with any assigned context and/or payload,
// and is free to handle it as it sees fit.
// note that if the message is a response, the process can issue a response again,
// and it will be directed to the same (remote) request as the original.
record send-error {
kind: send-error-kind,
message: message,
payload: option<payload>,
}
enum send-error-kind {
offline,
timeout,
}
// on-panic is a setting that determines what happens when a process panics.
// NOTE: requests should have expects-response set to false, will always be set to that by kernel
variant on-panic {
none,
restart,
requests(list<tuple<address, request, option<payload>>>),
}
}
world uq-process {
use types.{
json,
context,
address,
process-id,
payload,
request,
response,
message,
capabilities,
signed-capability,
send-error,
send-error-kind,
on-panic,
}
// entry point to all programs
export init: func(our: address)
// system utils:
import print-to-terminal: func(verbosity: u8, message: string)
import get-unix-time: func() -> u64
import get-eth-block: func() -> u64
// process management:
import set-on-panic: func(on-panic: on-panic)
import get-state: func() -> option<list<u8>>
import set-state: func(bytes: list<u8>)
import clear-state: func()
import spawn: func(id: process-id, %package: string, full-path: string, on-panic: on-panic, capabilities: capabilities) ->
option<process-id>
// capabilities management
// gives us all our signed capabilities so we can send them to others
import get-capabilities: func() -> list<signed-capability>
// gets a single specific capability
import get-capability: func(issuer: address, params: json) -> option<signed-capability>
// attaches a specific signed capability to our next message
import attach-capability: func(capability: signed-capability)
// saves capabilities to our store, so we can use them
import save-capabilities: func(capabilities: list<signed-capability>)
// check to see if the sender of a prompting message has a given capability, issued by us
// if the prompting message has a remote source, they must have attached it.
import has-capability: func(params: json) -> bool
// message I/O:
// ingest next message when it arrives along with its source.
// almost all long-running processes will call this in a loop
import receive: func() -> result<tuple<address, message>, tuple<send-error, option<context>>>
// gets payload, if any, of the message we just received
import get-payload: func() -> option<payload>
// send message(s) to target(s)
import send-request:
func(target: address, request: request, context: option<context>, payload: option<payload>)
import send-requests:
func(requests: list<tuple<address, request, option<context>, option<payload>>>)
import send-response:
func(response: response, payload: option<payload>)
// send a single request, then block (internally) until its response
// the type is Message but will always contain Response
import send-and-await-response:
func(target: address, request: request, payload: option<payload>) ->
result<tuple<address, message>, send-error>
// wasi
import wasi:random/insecure
import wasi:random/insecure-seed
import wasi:random/random
}

View File

@ -125,6 +125,12 @@ world uq-process {
import set-on-panic: func(on-panic: on-panic) import set-on-panic: func(on-panic: on-panic)
import get-state: func() -> option<list<u8>>
import set-state: func(bytes: list<u8>)
import clear-state: func()
import spawn: func(id: process-id, %package: string, full-path: string, on-panic: on-panic, capabilities: capabilities) -> import spawn: func(id: process-id, %package: string, full-path: string, on-panic: on-panic, capabilities: capabilities) ->
option<process-id> option<process-id>