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

Dynamic Userland Application Loading #3941

Open
wants to merge 16 commits into
base: master
Choose a base branch
from

Conversation

viswajith-g
Copy link

@viswajith-g viswajith-g commented Mar 28, 2024

Pull Request Overview

This pull request adds dynamic userland application loading to the kernel.

There are three stages to this:

  1. Setup Phase
  2. Flash Phase
  3. Load Phase

Setup Phase

During the setup phase, a userland application passes the size of the new binary to the app_loader capsule.
This capsule forwards the size to the kernel which determines if there is enough space in the flash to write the new app and what address the app should be written to. On success, the kernel returns the application size it has set up for to the capsule.
The capsule returns a success to the userland app.
On Failure, the kernel passes the reason for failure to the capsule, and the capsule passes a FAIL error to the userland app.

Flash Phase

After the userland app receives the Ok from the capsule, the app sends the binary of the new app 512 bytes at a time along with the offset.
The capsule checks that these values do not violate the bounds dictated by the kernel and then passes the binary data to the kernel.
Upon receiving the binary data, the kernel once again checks if the data is within the bounds specified during the setup phase and then writes the data to flash. On success, the kernel passes success to the app via the capsule. On failure, the same is propagated to the userland application.

Load Phase

Once the userland app receives confirmation that the final set of writes is completed, the app sends a load request to the kernel via the capsule. The kernel looks for the process binary at the address we just finished writing and converts the app binary into a process object. Upon success, the kernel tries to add the process object to the processes array. Once this is done, the process is officially active, and the kernel writes padding after the newly installed app to preserve the continuation of the linked list for future app loading.

Testing Strategy

This pull request was tested by running the app_loader application, a test userland app to verify if the new app (blink) was written and loaded correctly on nRF52840DK. This was tested in three configurations:

  1. When only the app_loader user app is installed on the device.
    • In this case, the dynamic app loader writes the new app after the app_loader user app and then loads it.
  2. When there is another app in addition to app_loader.
    • The device had two apps installed on it, one was adc and the other was app_loader. In this case, the dynamic process loader finds available flash after the two apps and writes blink there after which it loads the new app.
  3. When there is padding in between applications.
    • In this case, the device had padding, then app_loader and more padding. Dynamic process loader fits the app in the padding area before app_loader, and writes new padding between blink and app_loader.

A previous implementation of the process loader without the process binaries is available at this repo. This version was tested on the Imix board, and given not too much has changed, I think the new implementation should work on Imix.

TODO or Help Wanted

  1. More testing.
  2. Feedback and merge.

Documentation Updated

✅ Updated the relevant files in /docs, or no updates are required.

Formatting

✅ Ran make prepush.

This version works by writing the app first and then creates the process and adds it to the processes array using the synchronous process loading methods. Finally it writes the padding app. There are new overheads introduced however in the form of load_processes_return_memory() and load_processes_from_flash_return() in process_loading.rs. This is because, when the board initially does setup, we want it to pass the remaining RAM available to the dynamic process loader to load new apps. Currently load_processes() does not return the remaining memory, so those two new methods were added.
fixed some warnings causing CI fail
kernel/src/dynamic_process_loading.rs Outdated Show resolved Hide resolved
kernel/src/dynamic_process_loading.rs Outdated Show resolved Hide resolved
kernel/src/dynamic_process_loading.rs Outdated Show resolved Hide resolved
kernel/src/dynamic_process_loading.rs Outdated Show resolved Hide resolved
Ok(())
}

fn find_next_available_address(&self, new_app: &mut NewApp) -> Result<(), ErrorCode> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this function should take a slice and return the values, not take in a mutable object.

Copy link
Author

Choose a reason for hiding this comment

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

I think I've implemented it, please check.

Comment on lines 909 to 911
// reset the global new app start address and length values
self.new_app_start_addr.set(0);
self.new_app_length.set(0);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this needs to happen even if an error occurs above.

Copy link
Author

Choose a reason for hiding this comment

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

I think I fixed that now.

// thererfore we need to add the physical address of the new app
// for each write
} else {
offset // for padding, the kernel passes the address, so no need to add an offset
Copy link
Contributor

Choose a reason for hiding this comment

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

This is confusing. Maybe a separate function?

Copy link
Author

Choose a reason for hiding this comment

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

I created a separate function for the bounds check and the physical address calculation. It is in a place that I am slightly unhappy with, but it gets the job done.

if !padding_flag {
match command {
NonvolatileCommand::UserspaceRead | NonvolatileCommand::UserspaceWrite => {
if offset >= self.new_app_start_addr.get()
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this check do?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should compare to length.

NonvolatileCommand::UserspaceRead | NonvolatileCommand::UserspaceWrite => {
if offset >= self.new_app_start_addr.get()
|| length > self.new_app_length.get()
|| offset + length > self.new_app_length.get()
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be problematic if offset and/or length are very large: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ad881cb6ed56bfa015fcd78983987b6f

Previous checks should mitigate this.

Copy link
Author

Choose a reason for hiding this comment

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

added checked_add() to make sure we catch overflows.

Comment on lines 212 to 214
NonvolatileCommand::UserspaceWrite => {
self.driver.write(user_buffer, physical_address, active_len)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Where do we validate that we always end up with a valid TBF header in flash?

Copy link
Author

Choose a reason for hiding this comment

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

I'm going to work on this next. While at it, I also have some ideas on improving the state machine to ensure that the user app always tries to write to valid and successive address blocks.

Comment on lines 68 to 70
buffer: &'static mut [u8],
offset: usize,
app_size: usize,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you replace buffer and app_size with a SubSliceMut?

Copy link
Author

Choose a reason for hiding this comment

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

Do we want the capsule to do the sub slicing?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes

}
}

// Needs to be set separately, or breaks grants allocation
Copy link
Contributor

Choose a reason for hiding this comment

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

While I agree this needs to be a function, what does breaking grant allocation mean?

Copy link
Author

Choose a reason for hiding this comment

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

Explained better.


/******************************************* NVM Stuff **********************************************************/

// This function checks whether the new app will fit in the bounds dictated by the start address and length provided
Copy link
Contributor

Choose a reason for hiding this comment

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

Use /// comments for function comments.

kernel/src/dynamic_process_loading.rs Outdated Show resolved Hide resolved
kernel/src/dynamic_process_loading.rs Outdated Show resolved Hide resolved
kernel/src/dynamic_process_loading.rs Outdated Show resolved Hide resolved
kernel/src/dynamic_process_loading.rs Outdated Show resolved Hide resolved
kernel/src/dynamic_process_loading.rs Outdated Show resolved Hide resolved
kernel/src/dynamic_process_loading.rs Outdated Show resolved Hide resolved
changed it so that the capsule sends a subslice of the buffer to the kernel so the kernel does not have to compare lengths that the capsule provided vs the length of the buffer the capsule actually sent. Generally a safer approach.
Improved the state machine to better track if a userland app is requesting write to the bounds allocated to it during the setup phase. This state machine also ensures that when the process loader is busy, another request cannot be made.
Improved the state machine to better track if a userland app is requesting write to the bounds allocated to it during the setup phase. Also tracks if the dynamic process loader is currently busy. Performed some code cleanup.
The kernel now validates the header before writing the app into flash. In addition, upon failure at any stage, the flash region will be reclaimed for future use. Added a Fail state to help track failure modes better.
fixed two instances in app write when resets were not taking place. also fixed an improper condition check for declared length vs length in header.
An app that was successfully could not be made into a process object because of errant state tracking. That was fixed. Additionally, removed unnecessary state and parameter clears during Busy state.
@viswajith-g viswajith-g requested a review from bradjc April 2, 2024 02:40
// dynamic process loader to find the right spot to load the new app because otherwise
// we are not privy to this information.

#[inline(always)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why?

Copy link
Author

Choose a reason for hiding this comment

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

The current return type for load_processes is Result<(), ProcessLoadError>. We want the function to return the remaining memory when the board initializes the processes in main.rs. Currently, changing the return type breaks the build for all other boards.

What we require:

.unwrap_or_else(|(err, remaining_memory)| {
        debug!("Error loading processes!");
        debug!("{:?}", err);
        remaining_memory
    });

What we currently have:

.unwrap_or_else(|err| {
        debug!("Error loading processes!");
        debug!("{:?}", err);
    });

I have duplicated the functions needed to return the memory for this implementation, the other fix is to change the return type for all the supported boards.

Copy link
Contributor

Choose a reason for hiding this comment

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

I meant why #[inline(always)]

But in terms of duplicating a function, it would probably make more sense to just change the boards to ignore the return type instead of duplicating a function just to return something new

Copy link
Author

Choose a reason for hiding this comment

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

I removed the inline, forgot to remove it after copying the functions.

This is just to test if the dynamic process loading works, so I think it is less work to duplicate the functions to prove the idea than to change the return types for all boards.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's a lot more work to maintain duplicate functions

Copy link
Author

Choose a reason for hiding this comment

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

Right, but the synchronous functions will go away when we move to async loading. That will remove the need to change the return types. If we make the change now, we will have to revert later which is pointless. The current implementation takes a step forward in enabling dynamic process loading, but is not the end goal.

self.load_processes_from_flash(app_flash, app_memory)?;

if config::CONFIG.debug_process_credentials {
debug!("Checking: no checking, load and run all processes");
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't follow, how do we know there is no checking?

Copy link
Author

Choose a reason for hiding this comment

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

I have not implemented credential checking yet.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we not just use the existing process loader, which should do the checking for us? It seems strange to duplicate the loading

Copy link
Author

Choose a reason for hiding this comment

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

No, the current synchronous process loader does not perform process checking anyway. Plus we lose access to the current processes array after loading the new app if we do not duplicate the functions.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why can't we just modify the existing ones to not loose access?

Comment on lines +117 to +121
// load_processes_return_memory() and load_processes_from_flash_return are copies
// of load_processes_return_memory() and load_processes_from_flash(). These functions
// are created so that they return the remaining memory to main.rs after loading
// all the apps initially. This remaining_memory value is then used by the
// dynamic process loader to find the right spot to load the new app because otherwise
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just modify the existing ones instead of creating a copy?

Apps were previously aligned but we were not writing padding before new apps to match the linkedlist with the alignment. This is now fixed. A new enum was added to track the padding requirement. Additionally, the setup phase is now asynchronous to accomodate cases where you may or may not have to write padding after an app depending on if there is an app stored beyond our newly written app. This change propogates to the capsule as well.
check_padding_requirements was called twice once during postpad and during prepad to return the next_app_start_addr and previous_app_end_addr respectively. Changed the function so that the values are stored and the function does not have to be called twice.
changed the way the app address is identified during setup phase to align the app to the power of 2 based on its size and start address.
// Internal buffer for copying appslices into.
buffer: TakeCell<'static, [u8]>,
// What issued the currently executing call.
current_user: OptionalCell<NonvolatileUser>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
current_user: OptionalCell<NonvolatileUser>,
current_user: OptionalCell<ProcessId>,

Comment on lines 411 to 422
let result = self
.buffer
.take()
.map_or(Err(ErrorCode::RESERVE), |buffer| {
let mut write_buffer = SubSliceMut::new(buffer);
write_buffer.slice(..write_buffer.len());
let res = self.driver.write_app_data(write_buffer, offset, pid);
match res {
Ok(()) => Ok(()),
Err(e) => Err(e),
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Why doesn't enqueue() just handle this?


// And then signal the app.
kernel_data
.schedule_upcall(upcall::WRITE_DONE, (length, 0, 0))
Copy link
Contributor

Choose a reason for hiding this comment

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

First argument to userpsace should always be an errorcode (or 0 for success).

Comment on lines 219 to 224
if self.current_user.is_none() {
// No app is currently using the underlying storage.
// Mark this app as active, and then execute the command.
self.current_user.set(NonvolatileUser::App {
processid: processid,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this check if the processid matches the current user?

capsules/extra/src/app_loader.rs Show resolved Hide resolved
Comment on lines 1085 to 1091
// the app is trying to write to an address that is not the immediate continuation of where
// the previous write left off. Return an Invalid error.
if offset != self.next_offset.get() {
// check if the app is trying to write to the correct offset
self.reset_process_loading_metadata(); // clear out new app size, start address, current offset and state
return Err(ErrorCode::INVAL);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, I see. But what if the loader is doing compression where it doesn't need to write every byte?

Copy link
Author

Choose a reason for hiding this comment

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

I did not think of that

Comment on lines 1108 to 1113
Some(State::LastWrite)
| Some(State::Setup)
| Some(State::Load)
| Some(State::PaddingWrite)
| Some(State::Fail)
| None => Err(ErrorCode::BUSY),
Copy link
Contributor

Choose a reason for hiding this comment

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

Just _ => Err() is fine.

Comment on lines +423 to +424
self.previous_app_end_addr.set(previous_app_end_addr);
self.next_app_start_addr.set(next_app_start_addr);
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is called check_*, but it modifies internal state. That is somewhat confusing.

Copy link
Author

Choose a reason for hiding this comment

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

renamed it to compute_padding_requirements_and_neighbors()

Comment on lines +577 to +581
fn find_next_available_address(
&self,
flash: &'static [u8],
app_length: usize,
) -> Result<usize, ErrorCode> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would more straightforward to just have this function return more, and encompass all of the work of determining where an app can go and what padding requirements there are. So this would return the address and the padding needs.

Copy link
Author

Choose a reason for hiding this comment

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

I think padding needs should not be returnable because we are setting it using an OptionalCell. This way, we don't have to call the function again during load to check if we need to write pre-pad. (I know I am calling it right now, I should not be doing that because the value is set the first time anyway, so I removed the check_padding_req() from load).

self.new_app_length.set(app_length);

// check what kind of padding we have to write, no padding, pre padding, post padding or both pre and post padding
self.check_padding_requirements();
Copy link
Contributor

Choose a reason for hiding this comment

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

With check_padding_reqs integrated into find_next_available this code is a little more straightforward.

Copy link
Author

Choose a reason for hiding this comment

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

so we compute the requirements in find_next_address, but still write the padding in setup right? I find it odd otherwise that the function trying to find the next address and padding requirement also writes the padding. It makes sense that the main setup logic should take care of this.

Moved some logic to improve flow. Changed write so that we don't track write validity based on offset increments. Added checks to make sure the header is not manipulated without changing the whole header.
//! | userspace |
//! | |
//! +-----------------------------------------------------------------+
//! kernel::Driver
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
//! kernel::Driver
//! kernel::SyscallDriver

/// - Returns ErrorCode::FAIL if:
/// - The kernel is unable to create a process object for the application
/// - The kernel fails to write a padding app (thereby potentially breaking the linkedlist)

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change

Comment on lines +158 to +160
// Shorten the length if the application gave us nowhere to
// put it.
let active_len = cmp::min(length, allow_buf_len);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Shorten the length if the application gave us nowhere to
// put it.
let active_len = cmp::min(length, allow_buf_len);
// Shorten the length if the application did not give us
// enough bytes in the allowed buffer.
let active_len = cmp::min(length, allow_buf_len);

Comment on lines +94 to +111
pub struct DynamicProcessLoader<'a, C: 'static + Chip> {
kernel: &'static Kernel,
chip: &'static C,
fault_policy: &'static dyn ProcessFaultPolicy,
procs: MapCell<&'static mut [Option<&'static dyn process::Process>]>,
flash: Cell<&'static [u8]>,
app_memory: OptionalCell<&'static mut [u8]>,
new_process_flash: OptionalCell<&'static [u8]>,
flash_driver: &'a dyn NonvolatileStorage<'a>,
buffer: TakeCell<'static, [u8]>,
new_app_start_addr: Cell<usize>,
new_app_length: Cell<usize>,
previous_app_end_addr: Cell<usize>,
next_app_start_addr: Cell<usize>,
client: OptionalCell<&'static dyn DynamicProcessLoadingClient>,
state: Cell<State>,
padding_requirement: OptionalCell<PaddingRequirement>,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you move all of the state that is temporary (ie is only valid/set when an app is being loaded) to its own struct, and just have that entire struct in a cell (optionalcell)?

This is complicated enough it would help to be able to tell what is transient and what isn't.

Comment on lines +1021 to +1034
match self.find_next_available_address(flash, app_length) {
Ok(new_app_start_address) => {
let offset = new_app_start_address - flash_start;
let new_process_flash = self
.flash
.get()
.get(offset..offset + app_length)
.ok_or(ErrorCode::FAIL)?;

self.new_process_flash.set(new_process_flash);
self.new_app_start_addr.set(new_app_start_address);
self.new_app_length.set(app_length);

match self.padding_requirement.get() {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is too confusing. find_next_available_address() should just do a find and return everything (including the need for padding). That state is mutated deep in here makes it hard to reason about how this works.

Comment on lines +1054 to +1058
None | Some(PaddingRequirement::PrePad) => {
self.state.set(State::AppWrite);
self.client.map(|client| {
client.setup_done();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

We can't do this. We can't call a callback from a downcall (ie from DynamicProcessLoading.setup).

This needs to just return something that says "setup complete" (as opposed to "setup is still pending") or this file needs a deferred call to signal the callback.

// There is no header here
// if we fail here, let us erase the app we just wrote
self.state.set(State::PaddingWrite);
let _ = self.write_padding_app(
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume this won't work if we are already writing a padding app from line 1111?

Copy link
Author

Choose a reason for hiding this comment

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

It works, the padding app from line 1111 only writes prepad. This line erases the app that was written and postpad by extending the prepad to the next app's start address. Essentially we rewrite the prepad such that it reclaims the memory allocated to the new app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants