capsules_extra/hmac.rs
1// Licensed under the Apache License, Version 2.0 or the MIT License.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// Copyright Tock Contributors 2022.
4
5//! HMAC (Hash-based Message Authentication Code).
6//!
7//! Usage
8//! -----
9//!
10//! ```rust,ignore
11//! let hmac = &earlgrey::hmac::HMAC;
12//!
13//! let mux_hmac = static_init!(MuxHmac<'static, lowrisc::hmac::Hmac>, MuxHmac::new(hmac));
14//! digest::Digest::set_client(&earlgrey::hmac::HMAC, mux_hmac);
15//!
16//! let virtual_hmac_user = static_init!(
17//! VirtualMuxHmac<'static, lowrisc::hmac::Hmac>,
18//! VirtualMuxHmac::new(mux_hmac)
19//! );
20//! let hmac = static_init!(
21//! capsules::hmac::HmacDriver<'static, VirtualMuxHmac<'static, lowrisc::hmac::Hmac>>,
22//! capsules::hmac::HmacDriver::new(
23//! virtual_hmac_user,
24//! board_kernel.create_grant(&memory_allocation_cap),
25//! )
26//! );
27//! digest::Digest::set_client(virtual_hmac_user, hmac);
28//! ```
29
30use capsules_core::driver;
31use kernel::errorcode::into_statuscode;
32/// Syscall driver number.
33pub const DRIVER_NUM: usize = driver::NUM::Hmac as usize;
34
35/// Ids for read-only allow buffers
36mod ro_allow {
37 pub const KEY: usize = 0;
38 pub const DATA: usize = 1;
39 pub const COMPARE: usize = 2;
40 /// The number of allow buffers the kernel stores for this grant
41 pub const COUNT: u8 = 3;
42}
43
44/// Ids for read-write allow buffers
45mod rw_allow {
46 pub const DEST: usize = 2;
47 /// The number of allow buffers the kernel stores for this grant
48 pub const COUNT: u8 = 3;
49}
50
51use core::cell::Cell;
52
53use kernel::grant::{AllowRoCount, AllowRwCount, Grant, UpcallCount};
54use kernel::hil::digest;
55use kernel::processbuffer::{ReadableProcessBuffer, WriteableProcessBuffer};
56use kernel::syscall::{CommandReturn, SyscallDriver};
57use kernel::utilities::cells::{OptionalCell, TakeCell};
58use kernel::utilities::leasable_buffer::SubSlice;
59use kernel::utilities::leasable_buffer::SubSliceMut;
60use kernel::{ErrorCode, ProcessId};
61
62enum ShaOperation {
63 Sha256,
64 Sha384,
65 Sha512,
66}
67
68// Temporary buffer to copy the keys from userspace into
69//
70// Needs to be able to accommodate the largest key sizes, e.g. 512
71const TMP_KEY_BUFFER_SIZE: usize = 512 / 8;
72
73pub struct HmacDriver<'a, H: digest::Digest<'a, DIGEST_LEN>, const DIGEST_LEN: usize> {
74 hmac: &'a H,
75
76 active: Cell<bool>,
77
78 apps: Grant<
79 App,
80 UpcallCount<1>,
81 AllowRoCount<{ ro_allow::COUNT }>,
82 AllowRwCount<{ rw_allow::COUNT }>,
83 >,
84 processid: OptionalCell<ProcessId>,
85
86 data_buffer: TakeCell<'static, [u8]>,
87 data_copied: Cell<usize>,
88 dest_buffer: TakeCell<'static, [u8; DIGEST_LEN]>,
89}
90
91impl<
92 'a,
93 H: digest::Digest<'a, DIGEST_LEN>
94 + digest::HmacSha256
95 + digest::HmacSha384
96 + digest::HmacSha512,
97 const DIGEST_LEN: usize,
98 > HmacDriver<'a, H, DIGEST_LEN>
99{
100 pub fn new(
101 hmac: &'a H,
102 data_buffer: &'static mut [u8],
103 dest_buffer: &'static mut [u8; DIGEST_LEN],
104 grant: Grant<
105 App,
106 UpcallCount<1>,
107 AllowRoCount<{ ro_allow::COUNT }>,
108 AllowRwCount<{ rw_allow::COUNT }>,
109 >,
110 ) -> HmacDriver<'a, H, DIGEST_LEN> {
111 HmacDriver {
112 hmac,
113 active: Cell::new(false),
114 apps: grant,
115 processid: OptionalCell::empty(),
116 data_buffer: TakeCell::new(data_buffer),
117 data_copied: Cell::new(0),
118 dest_buffer: TakeCell::new(dest_buffer),
119 }
120 }
121
122 fn run(&self) -> Result<(), ErrorCode> {
123 self.processid.map_or(Err(ErrorCode::RESERVE), |processid| {
124 self.apps
125 .enter(processid, |app, kernel_data| {
126 kernel_data
127 .get_readonly_processbuffer(ro_allow::KEY)
128 .and_then(|key| {
129 key.enter(|k| {
130 if let Some(op) = &app.sha_operation {
131 let mut tmp_key_buffer: [u8; TMP_KEY_BUFFER_SIZE] =
132 [0; TMP_KEY_BUFFER_SIZE];
133 let key_len = core::cmp::min(k.len(), TMP_KEY_BUFFER_SIZE);
134 k[..key_len].copy_to_slice(&mut tmp_key_buffer[..key_len]);
135
136 match op {
137 ShaOperation::Sha256 => self
138 .hmac
139 .set_mode_hmacsha256(&tmp_key_buffer[..key_len]),
140 ShaOperation::Sha384 => self
141 .hmac
142 .set_mode_hmacsha384(&tmp_key_buffer[..key_len]),
143 ShaOperation::Sha512 => self
144 .hmac
145 .set_mode_hmacsha512(&tmp_key_buffer[..key_len]),
146 }
147 } else {
148 Err(ErrorCode::INVAL)
149 }
150 })
151 })
152 .unwrap_or(Err(ErrorCode::RESERVE))?;
153
154 kernel_data
155 .get_readonly_processbuffer(ro_allow::DATA)
156 .and_then(|data| {
157 data.enter(|data| {
158 let mut static_buffer_len = 0;
159 self.data_buffer.map(|buf| {
160 // Determine the size of the static buffer we have
161 static_buffer_len = buf.len();
162
163 if static_buffer_len > data.len() {
164 static_buffer_len = data.len()
165 }
166
167 self.data_copied.set(static_buffer_len);
168
169 // Copy the data into the static buffer
170 data[..static_buffer_len]
171 .copy_to_slice(&mut buf[..static_buffer_len]);
172 });
173
174 // Add the data from the static buffer to the HMAC
175 let mut lease_buf = SubSliceMut::new(
176 self.data_buffer.take().ok_or(ErrorCode::RESERVE)?,
177 );
178 lease_buf.slice(0..static_buffer_len);
179 if let Err(e) = self.hmac.add_mut_data(lease_buf) {
180 self.data_buffer.replace(e.1.take());
181 return Err(e.0);
182 }
183 Ok(())
184 })
185 })
186 .unwrap_or(Err(ErrorCode::RESERVE))
187 })
188 .unwrap_or_else(|err| Err(err.into()))
189 })
190 }
191
192 fn calculate_digest(&self) -> Result<(), ErrorCode> {
193 self.data_copied.set(0);
194
195 if let Err(e) = self
196 .hmac
197 .run(self.dest_buffer.take().ok_or(ErrorCode::RESERVE)?)
198 {
199 // Error, clear the processid and data
200 self.hmac.clear_data();
201 self.processid.clear();
202 self.dest_buffer.replace(e.1);
203
204 return Err(e.0);
205 }
206
207 Ok(())
208 }
209
210 fn verify_digest(&self) -> Result<(), ErrorCode> {
211 self.data_copied.set(0);
212
213 if let Err(e) = self
214 .hmac
215 .verify(self.dest_buffer.take().ok_or(ErrorCode::RESERVE)?)
216 {
217 // Error, clear the processid and data
218 self.hmac.clear_data();
219 self.processid.clear();
220 self.dest_buffer.replace(e.1);
221
222 return Err(e.0);
223 }
224
225 Ok(())
226 }
227
228 fn check_queue(&self) {
229 for appiter in self.apps.iter() {
230 let started_command = appiter.enter(|app, _| {
231 // If an app is already running let it complete
232 if self.processid.is_some() {
233 return true;
234 }
235
236 // If this app has a pending command let's use it.
237 app.pending_run_app.take().is_some_and(|processid| {
238 // Mark this driver as being in use.
239 self.processid.set(processid);
240 // Actually make the buzz happen.
241 self.run() == Ok(())
242 })
243 });
244 if started_command {
245 break;
246 }
247 }
248 }
249}
250
251impl<
252 'a,
253 H: digest::Digest<'a, DIGEST_LEN>
254 + digest::HmacSha256
255 + digest::HmacSha384
256 + digest::HmacSha512,
257 const DIGEST_LEN: usize,
258 > digest::ClientData<DIGEST_LEN> for HmacDriver<'a, H, DIGEST_LEN>
259{
260 // Because data needs to be copied from a userspace buffer into a kernel (RAM) one,
261 // we always pass mut data; this callback should never be invoked.
262 fn add_data_done(&self, _result: Result<(), ErrorCode>, _data: SubSlice<'static, u8>) {}
263
264 fn add_mut_data_done(&self, _result: Result<(), ErrorCode>, data: SubSliceMut<'static, u8>) {
265 self.processid.map(move |id| {
266 self.apps
267 .enter(id, move |app, kernel_data| {
268 let mut data_len = 0;
269 let mut exit = false;
270 let mut static_buffer_len = 0;
271
272 self.data_buffer.replace(data.take());
273
274 self.data_buffer.map(|buf| {
275 let ret = kernel_data
276 .get_readonly_processbuffer(ro_allow::DATA)
277 .and_then(|data| {
278 data.enter(|data| {
279 // Determine the size of the static buffer we have
280 static_buffer_len = buf.len();
281 // Determine how much data we have already copied
282 let copied_data = self.data_copied.get();
283
284 data_len = data.len();
285
286 if data_len > copied_data {
287 let remaining_data = &data[copied_data..];
288 let remaining_len = data_len - copied_data;
289
290 if remaining_len < static_buffer_len {
291 remaining_data.copy_to_slice(&mut buf[..remaining_len]);
292 } else {
293 remaining_data[..static_buffer_len].copy_to_slice(buf);
294 }
295 }
296 Ok(())
297 })
298 })
299 .unwrap_or(Err(ErrorCode::RESERVE));
300
301 if ret == Err(ErrorCode::RESERVE) {
302 // No data buffer, clear the processid and data
303 self.hmac.clear_data();
304 self.processid.clear();
305 exit = true;
306 }
307 });
308
309 if exit {
310 return;
311 }
312
313 if static_buffer_len > 0 {
314 let copied_data = self.data_copied.get();
315
316 if data_len > copied_data {
317 // Update the amount of data copied
318 self.data_copied.set(copied_data + static_buffer_len);
319
320 let mut lease_buf = SubSliceMut::new(self.data_buffer.take().unwrap());
321
322 // Add the data from the static buffer to the HMAC
323 if data_len < (copied_data + static_buffer_len) {
324 lease_buf.slice(..(data_len - copied_data))
325 }
326
327 if self.hmac.add_mut_data(lease_buf).is_err() {
328 // Error, clear the processid and data
329 self.hmac.clear_data();
330 self.processid.clear();
331 return;
332 }
333
334 // Return as we don't want to run the digest yet
335 return;
336 }
337 }
338
339 // If we get here we are ready to run the digest, reset the copied data
340 if app.op.get().unwrap() == UserSpaceOp::Run {
341 if let Err(e) = self.calculate_digest() {
342 let _ =
343 kernel_data.schedule_upcall(0, (into_statuscode(e.into()), 0, 0));
344 }
345 } else if app.op.get().unwrap() == UserSpaceOp::Verify {
346 let _ = kernel_data
347 .get_readonly_processbuffer(ro_allow::COMPARE)
348 .and_then(|compare| {
349 compare.enter(|compare| {
350 let mut static_buffer_len = 0;
351 self.dest_buffer.map(|buf| {
352 // Determine the size of the static buffer we have
353 static_buffer_len = buf.len();
354
355 if static_buffer_len > compare.len() {
356 static_buffer_len = compare.len()
357 }
358
359 self.data_copied.set(static_buffer_len);
360
361 // Copy the data into the static buffer
362 compare[..static_buffer_len]
363 .copy_to_slice(&mut buf[..static_buffer_len]);
364 });
365 })
366 });
367
368 if let Err(e) = self.verify_digest() {
369 let _ =
370 kernel_data.schedule_upcall(1, (into_statuscode(e.into()), 0, 0));
371 }
372 } else {
373 let _ = kernel_data.schedule_upcall(0, (0, 0, 0));
374 }
375 })
376 .map_err(|err| {
377 if err == kernel::process::Error::NoSuchApp
378 || err == kernel::process::Error::InactiveApp
379 {
380 self.processid.clear();
381 }
382 })
383 });
384
385 self.check_queue();
386 }
387}
388
389impl<
390 'a,
391 H: digest::Digest<'a, DIGEST_LEN>
392 + digest::HmacSha256
393 + digest::HmacSha384
394 + digest::HmacSha512,
395 const DIGEST_LEN: usize,
396 > digest::ClientHash<DIGEST_LEN> for HmacDriver<'a, H, DIGEST_LEN>
397{
398 fn hash_done(&self, result: Result<(), ErrorCode>, digest: &'static mut [u8; DIGEST_LEN]) {
399 self.processid.map(|id| {
400 self.apps
401 .enter(id, |_, kernel_data| {
402 self.hmac.clear_data();
403
404 let pointer = digest[0] as *mut u8;
405
406 let _ = kernel_data
407 .get_readwrite_processbuffer(rw_allow::DEST)
408 .and_then(|dest| {
409 dest.mut_enter(|dest| {
410 let len = dest.len();
411
412 if len < DIGEST_LEN {
413 dest.copy_from_slice(&digest[0..len]);
414 } else {
415 dest[0..DIGEST_LEN].copy_from_slice(digest);
416 }
417 })
418 });
419
420 let _ = match result {
421 Ok(()) => kernel_data.schedule_upcall(0, (0, pointer as usize, 0)),
422 Err(e) => kernel_data
423 .schedule_upcall(0, (into_statuscode(e.into()), pointer as usize, 0)),
424 };
425
426 // Clear the current processid as it has finished running
427 self.processid.clear();
428 })
429 .map_err(|err| {
430 if err == kernel::process::Error::NoSuchApp
431 || err == kernel::process::Error::InactiveApp
432 {
433 self.processid.clear();
434 }
435 })
436 });
437
438 self.check_queue();
439 self.dest_buffer.replace(digest);
440 }
441}
442
443impl<
444 'a,
445 H: digest::Digest<'a, DIGEST_LEN>
446 + digest::HmacSha256
447 + digest::HmacSha384
448 + digest::HmacSha512,
449 const DIGEST_LEN: usize,
450 > digest::ClientVerify<DIGEST_LEN> for HmacDriver<'a, H, DIGEST_LEN>
451{
452 fn verification_done(
453 &self,
454 result: Result<bool, ErrorCode>,
455 compare: &'static mut [u8; DIGEST_LEN],
456 ) {
457 self.processid.map(|id| {
458 self.apps
459 .enter(id, |_app, kernel_data| {
460 self.hmac.clear_data();
461
462 let _ = match result {
463 Ok(equal) => kernel_data.schedule_upcall(1, (0, equal as usize, 0)),
464 Err(e) => kernel_data.schedule_upcall(1, (into_statuscode(e.into()), 0, 0)),
465 };
466
467 // Clear the current processid as it has finished running
468 self.processid.clear();
469 })
470 .map_err(|err| {
471 if err == kernel::process::Error::NoSuchApp
472 || err == kernel::process::Error::InactiveApp
473 {
474 self.processid.clear();
475 }
476 })
477 });
478
479 self.check_queue();
480 self.dest_buffer.replace(compare);
481 }
482}
483
484/// Specify memory regions to be used.
485///
486/// ### `allow_num`
487///
488/// - `0`: Allow a buffer for storing the key. The kernel will read from this
489/// when running This should not be changed after running `run` until the HMAC
490/// has completed
491/// - `1`: Allow a buffer for storing the buffer. The kernel will read from this
492/// when running This should not be changed after running `run` until the HMAC
493/// has completed
494/// - `2`: Allow a buffer for storing the digest. The kernel will fill this with
495/// the HMAC digest before calling the `hash_done` callback.
496impl<
497 'a,
498 H: digest::Digest<'a, DIGEST_LEN>
499 + digest::HmacSha256
500 + digest::HmacSha384
501 + digest::HmacSha512,
502 const DIGEST_LEN: usize,
503 > SyscallDriver for HmacDriver<'a, H, DIGEST_LEN>
504{
505 // Subscribe to HmacDriver events.
506 //
507 // ### `subscribe_num`
508 //
509 // - `0`: Subscribe to interrupts from HMAC events. The callback signature
510 // is `fn(result: u32)`
511
512 /// Setup and run the HMAC hardware
513 ///
514 /// We expect userspace to setup buffers for the key, data and digest.
515 /// These buffers must be allocated and specified to the kernel from the
516 /// above allow calls.
517 ///
518 /// We expect userspace not to change the value while running. If userspace
519 /// changes the value we have no guarantee of what is passed to the
520 /// hardware. This isn't a security issue, it will just prove the requesting
521 /// app with invalid data.
522 ///
523 /// The driver will take care of clearing data from the underlying implementation
524 /// by calling the `clear_data()` function when the `hash_complete()` callback
525 /// is called or if an error is encountered.
526 ///
527 /// ### `command_num`
528 ///
529 /// - `0`: set_algorithm
530 /// - `1`: run
531 /// - `2`: update
532 /// - `3`: finish
533 fn command(
534 &self,
535 command_num: usize,
536 data1: usize,
537 _data2: usize,
538 processid: ProcessId,
539 ) -> CommandReturn {
540 let match_or_empty_or_nonexistant = self.processid.map_or(true, |owning_app| {
541 // We have recorded that an app has ownership of the HMAC.
542
543 // If the HMAC is still active, then we need to wait for the operation
544 // to finish and the app, whether it exists or not (it may have crashed),
545 // still owns this capsule. If the HMAC is not active, then
546 // we need to verify that that application still exists, and remove
547 // it as owner if not.
548 if self.active.get() {
549 owning_app == processid
550 } else {
551 // Check the app still exists.
552 //
553 // If the `.enter()` succeeds, then the app is still valid, and
554 // we can check if the owning app matches the one that called
555 // the command. If the `.enter()` fails, then the owning app no
556 // longer exists and we return `true` to signify the
557 // "or_nonexistant" case.
558 self.apps
559 .enter(owning_app, |_, _| owning_app == processid)
560 .unwrap_or(true)
561 }
562 });
563
564 let app_match = self.processid.map_or(false, |owning_app| {
565 // We have recorded that an app has ownership of the HMAC.
566
567 // If the HMAC is still active, then we need to wait for the operation
568 // to finish and the app, whether it exists or not (it may have crashed),
569 // still owns this capsule. If the HMAC is not active, then
570 // we need to verify that that application still exists, and remove
571 // it as owner if not.
572 if self.active.get() {
573 owning_app == processid
574 } else {
575 // Check the app still exists.
576 //
577 // If the `.enter()` succeeds, then the app is still valid, and
578 // we can check if the owning app matches the one that called
579 // the command. If the `.enter()` fails, then the owning app no
580 // longer exists and we return `true` to signify the
581 // "or_nonexistant" case.
582 self.apps
583 .enter(owning_app, |_, _| owning_app == processid)
584 .unwrap_or(true)
585 }
586 });
587
588 // Try the commands where we want to start an operation *not* entered in
589 // an app grant first.
590 if match_or_empty_or_nonexistant
591 && (command_num == 1 || command_num == 2 || command_num == 4)
592 {
593 self.processid.set(processid);
594
595 let _ = self.apps.enter(processid, |app, _| {
596 if command_num == 1 {
597 // run
598 // Use key and data to compute hash
599 // This will trigger a callback once the digest is generated
600 app.op.set(Some(UserSpaceOp::Run));
601 } else if command_num == 2 {
602 // update
603 // Input key and data, don't compute final hash yet
604 // This will trigger a callback once the data has been added.
605 app.op.set(Some(UserSpaceOp::Update));
606 } else if command_num == 4 {
607 // verify
608 // Use key and data to compute hash and comapre it against
609 // the digest
610 app.op.set(Some(UserSpaceOp::Verify));
611 }
612 });
613
614 return if let Err(e) = self.run() {
615 self.hmac.clear_data();
616 self.processid.clear();
617 self.check_queue();
618 CommandReturn::failure(e)
619 } else {
620 CommandReturn::success()
621 };
622 }
623
624 self.apps
625 .enter(processid, |app, kernel_data| {
626 match command_num {
627 // set_algorithm
628 0 => {
629 match data1 {
630 // SHA256
631 0 => {
632 app.sha_operation = Some(ShaOperation::Sha256);
633 CommandReturn::success()
634 }
635 // SHA384
636 1 => {
637 app.sha_operation = Some(ShaOperation::Sha384);
638 CommandReturn::success()
639 }
640 // SHA512
641 2 => {
642 app.sha_operation = Some(ShaOperation::Sha512);
643 CommandReturn::success()
644 }
645 _ => CommandReturn::failure(ErrorCode::NOSUPPORT),
646 }
647 }
648
649 // run
650 1 => {
651 // There is an active app, so queue this request (if possible).
652 if app.pending_run_app.is_some() {
653 // No more room in the queue, nowhere to store this
654 // request.
655 CommandReturn::failure(ErrorCode::NOMEM)
656 } else {
657 // We can store this, so lets do it.
658 app.pending_run_app = Some(processid);
659 app.op.set(Some(UserSpaceOp::Run));
660 CommandReturn::success()
661 }
662 }
663
664 // update
665 2 => {
666 // There is an active app, so queue this request (if possible).
667 if app.pending_run_app.is_some() {
668 // No more room in the queue, nowhere to store this
669 // request.
670 CommandReturn::failure(ErrorCode::NOMEM)
671 } else {
672 // We can store this, so lets do it.
673 app.pending_run_app = Some(processid);
674 app.op.set(Some(UserSpaceOp::Update));
675 CommandReturn::success()
676 }
677 }
678
679 // finish
680 // Compute final hash yet, useful after a update command
681 3 => {
682 if app_match {
683 if let Err(e) = self.calculate_digest() {
684 let _ = kernel_data.schedule_upcall(
685 0,
686 (kernel::errorcode::into_statuscode(e.into()), 0, 0),
687 );
688 }
689 CommandReturn::success()
690 } else {
691 // We don't queue this request, the user has to call
692 // `update` first.
693 CommandReturn::failure(ErrorCode::OFF)
694 }
695 }
696
697 // verify
698 4 => {
699 // There is an active app, so queue this request (if possible).
700 if app.pending_run_app.is_some() {
701 // No more room in the queue, nowhere to store this
702 // request.
703 CommandReturn::failure(ErrorCode::NOMEM)
704 } else {
705 // We can store this, so lets do it.
706 app.pending_run_app = Some(processid);
707 app.op.set(Some(UserSpaceOp::Verify));
708 CommandReturn::success()
709 }
710 }
711
712 // verify_finish
713 // Use key and data to compute hash and compare it against
714 // the digest, useful after a update command
715 5 => {
716 if app_match {
717 let _ = kernel_data
718 .get_readonly_processbuffer(ro_allow::COMPARE)
719 .and_then(|compare| {
720 compare.enter(|compare| {
721 let mut static_buffer_len = 0;
722 self.dest_buffer.map(|buf| {
723 // Determine the size of the static buffer we have
724 static_buffer_len = buf.len();
725
726 if static_buffer_len > compare.len() {
727 static_buffer_len = compare.len()
728 }
729
730 self.data_copied.set(static_buffer_len);
731
732 // Copy the data into the static buffer
733 compare[..static_buffer_len]
734 .copy_to_slice(&mut buf[..static_buffer_len]);
735 });
736 })
737 });
738
739 if let Err(e) = self.verify_digest() {
740 let _ = kernel_data
741 .schedule_upcall(1, (into_statuscode(e.into()), 0, 0));
742 }
743 CommandReturn::success()
744 } else {
745 // We don't queue this request, the user has to call
746 // `update` first.
747 CommandReturn::failure(ErrorCode::OFF)
748 }
749 }
750
751 // default
752 _ => CommandReturn::failure(ErrorCode::NOSUPPORT),
753 }
754 })
755 .unwrap_or_else(|err| err.into())
756 }
757
758 fn allocate_grant(&self, processid: ProcessId) -> Result<(), kernel::process::Error> {
759 self.apps.enter(processid, |_, _| {})
760 }
761}
762
763#[derive(Copy, Clone, PartialEq)]
764enum UserSpaceOp {
765 Run,
766 Update,
767 Verify,
768}
769
770#[derive(Default)]
771pub struct App {
772 pending_run_app: Option<ProcessId>,
773 sha_operation: Option<ShaOperation>,
774 op: Cell<Option<UserSpaceOp>>,
775}