kernel/ipc.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//! Inter-process communication mechanism for Tock.
6//!
7//! This is a special syscall driver that allows userspace applications to
8//! share memory.
9
10use crate::ErrorCode;
11use crate::capabilities::MemoryAllocationCapability;
12use crate::grant::{AllowRoCount, AllowRwCount, Grant, UpcallCount};
13use crate::kernel::Kernel;
14use crate::process;
15use crate::process::ProcessId;
16use crate::processbuffer::ReadableProcessBuffer;
17use crate::syscall_driver::{CommandReturn, SyscallDriver};
18
19/// Syscall number
20pub const DRIVER_NUM: usize = 0x10000;
21
22/// Ids for read-only allow buffers
23mod ro_allow {
24 pub(super) const SEARCH: usize = 0;
25 /// The number of allow buffers the kernel stores for this grant.
26 pub(super) const COUNT: u8 = 1;
27}
28
29/// Enum to mark which type of upcall is scheduled for the IPC mechanism.
30#[derive(Copy, Clone, Debug)]
31pub enum IPCUpcallType {
32 /// Indicates that the upcall is for the service upcall handler this
33 /// process has setup.
34 Service,
35 /// Indicates that the upcall is from a different service app and will
36 /// call one of the client upcalls setup by this process.
37 Client,
38}
39
40/// State that is stored in each process's grant region to support IPC.
41#[derive(Default)]
42struct IPCData;
43
44/// The IPC mechanism struct.
45pub struct IPC<const NUM_PROCS: u8> {
46 /// The grant regions for each process that holds the per-process IPC data.
47 data: Grant<
48 IPCData,
49 UpcallCount<NUM_PROCS>,
50 AllowRoCount<{ ro_allow::COUNT }>,
51 AllowRwCount<NUM_PROCS>,
52 >,
53}
54
55impl<const NUM_PROCS: u8> IPC<NUM_PROCS> {
56 pub fn new(
57 kernel: &'static Kernel,
58 driver_num: usize,
59 capability: &dyn MemoryAllocationCapability,
60 ) -> Self {
61 Self {
62 data: kernel.create_grant(driver_num, capability),
63 }
64 }
65
66 /// Schedule an IPC upcall for a process. This is called by the main
67 /// scheduler loop if an IPC task was queued for the process.
68 ///
69 /// # Safety
70 ///
71 /// Scheduling the upcall is what causes a shared buffer to be shared with
72 /// the target (`schedule_on`) process. This causes an MPU region to be
73 /// created for the target process, giving it access to the shared buffer.
74 ///
75 /// The caller must ensure that source (`called_from`) process is not
76 /// deallocated/reclaimed while the buffer is still shared (i.e., the MPU
77 /// region is mapped). Failing to ensure this could potentially give the
78 /// original target process access to memory allocated for a new process.
79 /// More problematically, that memory could be used for the kernel's grant
80 /// region for a new process, and allowing a process to access that memory
81 /// violates memory safety for the kernel.
82 pub(crate) unsafe fn schedule_upcall(
83 &self,
84 schedule_on: ProcessId,
85 called_from: ProcessId,
86 cb_type: IPCUpcallType,
87 ) -> Result<(), process::Error> {
88 let schedule_on_id = schedule_on.index().ok_or(process::Error::NoSuchApp)?;
89 let called_from_id = called_from.index().ok_or(process::Error::NoSuchApp)?;
90
91 // Verify that IPC is not trying to share with the same app. If so, this
92 // will cause a double grant enter if we don't return now.
93 if schedule_on_id == called_from_id {
94 return Err(process::Error::AlreadyInUse);
95 }
96
97 self.data.enter(schedule_on, |_, schedule_on_data| {
98 self.data.enter(called_from, |_, called_from_data| {
99 // If the other app shared a buffer with us, make
100 // sure we have access to that slice and then call
101 // the upcall. If no slice was shared then just
102 // call the upcall.
103 let (len, ptr) = match called_from_data.get_readwrite_processbuffer(schedule_on_id)
104 {
105 Ok(slice) => {
106 // Ensure receiving app has MPU access to sending app's buffer
107 self.data
108 .kernel
109 .process_map_or(None, schedule_on, |process| {
110 process.add_mpu_region(slice.ptr(), slice.len(), slice.len())
111 });
112 (slice.len(), slice.ptr() as usize)
113 }
114 Err(_) => (0, 0),
115 };
116 let to_schedule: usize = match cb_type {
117 IPCUpcallType::Service => schedule_on_id,
118 IPCUpcallType::Client => called_from_id,
119 };
120 let _ = schedule_on_data.schedule_upcall(to_schedule, (called_from_id, len, ptr));
121 })
122 })?
123 }
124}
125
126impl<const NUM_PROCS: u8> SyscallDriver for IPC<NUM_PROCS> {
127 /// command is how notify() is implemented.
128 /// Notifying an IPC service is done by setting client_or_svc to 0,
129 /// and notifying an IPC client is done by setting client_or_svc to 1.
130 /// In either case, the target_id is the same number as provided in a notify
131 /// upcall or as returned by allow.
132 ///
133 /// Returns INVAL if the other process doesn't exist.
134 ///
135 /// Initiates a service discovery or notifies a client or service.
136 ///
137 /// ### `command_num`
138 ///
139 /// - `0`: Driver existence check, always returns Ok(())
140 /// - `1`: Perform discovery on the package name passed to `allow_readonly`.
141 /// Returns the service descriptor if the service is found, otherwise
142 /// returns an error.
143 /// - `2`: Notify a service previously discovered to have the service
144 /// descriptor in `target_id`. Returns an error if `target_id` refers to
145 /// an invalid service or the notify fails to enqueue.
146 /// - `3`: Notify a client with descriptor `target_id`, typically in
147 /// response to a previous notify from the client. Returns an error if
148 /// `target_id` refers to an invalid client or the notify fails to
149 /// enqueue.
150 fn command(
151 &self,
152 command_number: usize,
153 target_id: usize,
154 _: usize,
155 processid: ProcessId,
156 ) -> CommandReturn {
157 match command_number {
158 0 => CommandReturn::success(),
159 1 =>
160 /* Discover */
161 {
162 self.data
163 .enter(processid, |_, kernel_data| {
164 kernel_data
165 .get_readonly_processbuffer(ro_allow::SEARCH)
166 .and_then(|search| {
167 search.enter(|slice| {
168 self.data
169 .kernel
170 .process_until(|p| {
171 let s = p.get_process_name().as_bytes();
172 // are slices equal?
173 if s.len() == slice.len()
174 && s.iter()
175 .zip(slice.iter())
176 .all(|(c1, c2)| *c1 == c2.get())
177 {
178 // Return the index of the process which is used for
179 // subscribe number
180 p.processid()
181 .index()
182 .map(|i| CommandReturn::success_u32(i as u32))
183 } else {
184 None
185 }
186 })
187 .unwrap_or(CommandReturn::failure(ErrorCode::NODEVICE))
188 })
189 })
190 .unwrap_or(CommandReturn::failure(ErrorCode::INVAL))
191 })
192 .unwrap_or(CommandReturn::failure(ErrorCode::NOMEM))
193 }
194 2 =>
195 /* Service notify */
196 {
197 let cb_type = IPCUpcallType::Service;
198
199 let other_process =
200 self.data
201 .kernel
202 .process_until(|p| match p.processid().index() {
203 Some(i) if i == target_id => Some(p.processid()),
204 _ => None,
205 });
206
207 other_process.map_or(CommandReturn::failure(ErrorCode::INVAL), |otherapp| {
208 self.data.kernel.process_map_or(
209 CommandReturn::failure(ErrorCode::INVAL),
210 otherapp,
211 |target| {
212 let ret = target.enqueue_task(process::Task::IPC((processid, cb_type)));
213 match ret {
214 Ok(()) => CommandReturn::success(),
215 Err(e) => {
216 // `enqueue_task` does not provide information on whether the
217 // recipient has set a non-null callback. It only reports
218 // general failures, such as insufficient memory in the pending
219 // tasks queue
220 CommandReturn::failure(e)
221 }
222 }
223 },
224 )
225 })
226 }
227 3 =>
228 /* Client notify */
229 {
230 let cb_type = IPCUpcallType::Client;
231
232 let other_process =
233 self.data
234 .kernel
235 .process_until(|p| match p.processid().index() {
236 Some(i) if i == target_id => Some(p.processid()),
237 _ => None,
238 });
239
240 other_process.map_or(CommandReturn::failure(ErrorCode::INVAL), |otherapp| {
241 self.data.kernel.process_map_or(
242 CommandReturn::failure(ErrorCode::INVAL),
243 otherapp,
244 |target| {
245 let ret = target.enqueue_task(process::Task::IPC((processid, cb_type)));
246 match ret {
247 Ok(()) => CommandReturn::success(),
248 Err(e) => {
249 // `enqueue_task` does not provide information on whether the
250 // recipient has set a non-null callback. It only reports
251 // general failures, such as insufficient memory in the pending
252 // tasks queue
253 CommandReturn::failure(e)
254 }
255 }
256 },
257 )
258 })
259 }
260 _ => CommandReturn::failure(ErrorCode::NOSUPPORT),
261 }
262 }
263
264 fn allocate_grant(&self, processid: ProcessId) -> Result<(), crate::process::Error> {
265 self.data.enter(processid, |_, _| {})
266 }
267}