fix: storage tables grow (handles, open files, encrypted opens, grants: Slots/Vec, no 8/16 caps; release follows all narrowed grants); storage says FULL, init spawn STORAGE_BUSY (was NOT_FOUND), pm BUSY; cap slots per process 64 -> 255

This commit is contained in:
2026-10-28 16:17:36 +01:00
parent 3b71f29f4c
commit 1f2134fcec
19 changed files with 208 additions and 128 deletions

View File

@@ -216,7 +216,7 @@ impl PermissionServer {
let spawned = libkos::spawn::spawn(self.init, file, Limits { pages: plan.app.memory.pages(), ..PROGRAM });
let _ = libkos::fs::release(file);
let child = spawned.map_err(|e| match e {
libkos::spawn::SpawnError::OverAllowance => code(reply::BUSY),
libkos::spawn::SpawnError::OverAllowance | libkos::spawn::SpawnError::StorageBusy => code(reply::BUSY),
_ => code(reply::FAILED),
})?;

View File

@@ -12,9 +12,7 @@
use filecrypt::{CHUNK, Drbg, HEADER, Header, Key, OVERHEAD, PLAIN_CHUNK, area_key, chunk_offset, chunks, plain_len};
use crate::filesystem::{DirEntry, FileHandle, FileSystem};
/// The inner file system's handles this layer can track.
const MAX_HANDLES: usize = 16;
use crate::slots::Slots;
/// Where a path falls.
#[derive(Debug, PartialEq, Eq)]
@@ -66,7 +64,8 @@ pub struct Encrypted<F: FileSystem> {
user: [u8; USER_NAME],
user_len: usize,
rng: Option<Drbg>,
open: [Option<Open>; MAX_HANDLES],
/// Sealed files open, by the inner file system's handle.
open: Slots<Open>,
}
/// A user folder's name: 16 hex digits, or `guest`.
@@ -74,7 +73,7 @@ pub const USER_NAME: usize = 16;
impl<F: FileSystem> Encrypted<F> {
pub fn new(inner: F) -> Self {
Self { inner, master: None, user: [0; USER_NAME], user_len: 0, rng: None, open: [const { None }; MAX_HANDLES] }
Self { inner, master: None, user: [0; USER_NAME], user_len: 0, rng: None, open: Slots::new() }
}
pub fn inner(&self) -> &F {
@@ -114,10 +113,6 @@ impl<F: FileSystem> Encrypted<F> {
}
}
fn slot(&mut self, h: FileHandle) -> Option<&mut Option<Open>> {
self.open.get_mut(h.raw() as usize)
}
/// Chunk `index` of a file of `size` bytes, opened into `out`; its length.
fn read_chunk(inner: &mut F, h: FileHandle, key: &Key, size: usize, index: usize, out: &mut [u8; PLAIN_CHUNK]) -> Option<usize> {
let last = index + 1 == chunks(size);
@@ -153,10 +148,10 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
}
fn lock(&mut self) {
for (i, slot) in self.open.iter_mut().enumerate() {
if slot.take().is_some() {
self.inner.close(FileHandle::new(i as u16));
}
let open: alloc::vec::Vec<usize> = self.open.iter().map(|(i, _)| i).collect();
for i in open {
self.open.take(i);
self.inner.close(FileHandle::new(i as u16));
}
self.master = None;
self.rng = None;
@@ -183,7 +178,7 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
return None;
};
let key = header.file_key(&area);
*self.slot(h)? = Some(Open { key, size, pos: 0 });
self.open.put(h.raw() as usize, Open { key, size, pos: 0 });
Some(h)
}
@@ -203,12 +198,12 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
let _ = self.inner.delete(path);
return None;
}
*self.slot(h)? = Some(Open { key: file_key, size: 0, pos: 0 });
self.open.put(h.raw() as usize, Open { key: file_key, size: 0, pos: 0 });
Some(h)
}
fn read(&mut self, h: FileHandle, buf: &mut [u8]) -> Option<usize> {
let Some(Some(f)) = self.open.get(h.raw() as usize) else { return self.inner.read(h, buf) };
let Some(f) = self.open.get(h.raw() as usize) else { return self.inner.read(h, buf) };
let (key, size, mut pos) = (f.key.clone(), f.size, f.pos);
let mut done = 0;
let mut chunk = [0u8; PLAIN_CHUNK];
@@ -222,12 +217,12 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
pos += take;
}
chunk.fill(0);
self.slot(h)?.as_mut()?.pos = pos;
self.open.get_mut(h.raw() as usize)?.pos = pos;
Some(done)
}
fn write(&mut self, h: FileHandle, buf: &[u8]) -> Option<usize> {
let Some(Some(f)) = self.open.get(h.raw() as usize) else { return self.inner.write(h, buf) };
let Some(f) = self.open.get(h.raw() as usize) else { return self.inner.write(h, buf) };
let (key, old_size, pos) = (f.key.clone(), f.size, f.pos);
let new_size = old_size.max(pos + buf.len());
let (old_last, new_last) = (chunks(old_size) - 1, chunks(new_size) - 1);
@@ -253,34 +248,32 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
}
}
chunk.fill(0);
let f = self.slot(h)?.as_mut()?;
let f = self.open.get_mut(h.raw() as usize)?;
f.size = new_size;
f.pos = pos + buf.len();
Some(buf.len())
}
fn seek(&mut self, h: FileHandle, offset: usize) -> bool {
match self.slot(h) {
Some(Some(f)) if offset <= f.size => {
match self.open.get_mut(h.raw() as usize) {
Some(f) if offset <= f.size => {
f.pos = offset;
true
}
Some(Some(_)) => false,
_ => self.inner.seek(h, offset),
Some(_) => false,
None => self.inner.seek(h, offset),
}
}
fn close(&mut self, h: FileHandle) {
if let Some(slot) = self.slot(h) {
*slot = None;
}
self.open.take(h.raw() as usize);
self.inner.close(h);
}
fn file_size(&mut self, h: FileHandle) -> Option<usize> {
match self.open.get(h.raw() as usize) {
Some(Some(f)) => Some(f.size),
_ => self.inner.file_size(h),
Some(f) => Some(f.size),
None => self.inner.file_size(h),
}
}

View File

@@ -9,11 +9,11 @@ mod path;
use port_block_device::BlockDevice;
use crate::filesystem::{DirEntry, FileHandle, FileSystem};
use crate::slots::Slots;
use io::read_block;
use superblock::Superblock;
const ROOT_INODE: u32 = 2;
const MAX_OPEN_FILES: usize = 8;
const EXTENT_MAGIC: u16 = 0xF30A;
@@ -31,7 +31,8 @@ pub struct Ext4<B: BlockDevice> {
device: B,
sb: Superblock,
gdt_block: u32,
open_files: [Option<OpenFile>; MAX_OPEN_FILES],
/// Files open, by handle: as many as storage's memory holds.
open_files: Slots<OpenFile>,
}
impl<B: BlockDevice> Ext4<B> {
@@ -47,7 +48,7 @@ impl<B: BlockDevice> Ext4<B> {
device,
sb,
gdt_block,
open_files: [const { None }; MAX_OPEN_FILES],
open_files: Slots::new(),
})
}
@@ -86,7 +87,7 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
if !inode.is_file() { return None; }
let handle = self.alloc_handle()?;
self.open_files[handle.raw() as usize] = Some(OpenFile {
self.open_files.put(handle.raw() as usize, OpenFile {
inode_num,
size: inode.size as usize,
offset: 0,
@@ -96,8 +97,7 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
}
fn read(&mut self, handle: FileHandle, buf: &mut [u8]) -> Option<usize> {
if handle.raw() as usize >= MAX_OPEN_FILES { return None; }
let file = *self.open_files[handle.raw() as usize].as_ref()?;
let file = *self.open_files.get(handle.raw() as usize)?;
if file.offset >= file.size { return Some(0); }
// As much as `buf` holds, across blocks: a short read means the end.
@@ -131,15 +131,14 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
done += chunk;
}
if let Some(f) = self.open_files[handle.raw() as usize].as_mut() {
if let Some(f) = self.open_files.get_mut(handle.raw() as usize) {
f.offset += done;
}
Some(done)
}
fn write(&mut self, handle: FileHandle, buf: &[u8]) -> Option<usize> {
if handle.raw() as usize >= MAX_OPEN_FILES { return None; }
let file = *self.open_files[handle.raw() as usize].as_ref()?;
let file = *self.open_files.get(handle.raw() as usize)?;
let bs = self.sb.block_size;
let mut written = 0;
let mut offset = file.offset;
@@ -186,7 +185,7 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
}
if written > 0 {
if let Some(f) = self.open_files[handle.raw() as usize].as_mut() {
if let Some(f) = self.open_files.get_mut(handle.raw() as usize) {
f.offset = offset;
f.size = size;
f.block_data = block_data;
@@ -205,8 +204,7 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
}
fn close(&mut self, handle: FileHandle) {
if handle.raw() as usize >= MAX_OPEN_FILES { return; }
if let Some(file) = self.open_files[handle.raw() as usize].take()
if let Some(file) = self.open_files.take(handle.raw() as usize)
&& let Some(mut inode) = self.read_inode(file.inode_num) {
inode.size = file.size as u64;
inode.block_data = file.block_data;
@@ -255,7 +253,7 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
}
let handle = self.alloc_handle()?;
self.open_files[handle.raw() as usize] = Some(OpenFile {
self.open_files.put(handle.raw() as usize, OpenFile {
inode_num: new_inode_num,
size: 0,
offset: 0,
@@ -297,18 +295,15 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
}
self.clear_inode(child_inode_num);
self.free_inode(child_inode_num);
for slot in self.open_files.iter_mut() {
if let Some(f) = slot
&& f.inode_num == child_inode_num {
*slot = None;
}
let gone: alloc::vec::Vec<usize> = self.open_files.iter().filter(|(_, f)| f.inode_num == child_inode_num).map(|(i, _)| i).collect();
for i in gone {
self.open_files.take(i);
}
true
}
fn file_size(&mut self, handle: FileHandle) -> Option<usize> {
if handle.raw() as usize >= MAX_OPEN_FILES { return None; }
self.open_files[handle.raw() as usize].as_ref().map(|f| f.size)
self.open_files.get(handle.raw() as usize).map(|f| f.size)
}
fn mkdir(&mut self, path: &[u8]) -> bool {
@@ -370,7 +365,7 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
}
fn seek(&mut self, handle: FileHandle, offset: usize) -> bool {
match self.open_files.get_mut(handle.raw() as usize).and_then(Option::as_mut) {
match self.open_files.get_mut(handle.raw() as usize) {
Some(f) if offset <= f.size => {
f.offset = offset;
true

View File

@@ -1,6 +1,6 @@
//! Path resolution, directory entry operations and allocation wrappers.
use super::{bitmap, dir, Ext4, MAX_OPEN_FILES, ROOT_INODE};
use super::{bitmap, dir, Ext4, ROOT_INODE};
use crate::filesystem::FileHandle;
use port_block_device::BlockDevice;
@@ -58,12 +58,7 @@ impl<B: BlockDevice> Ext4<B> {
}
pub(super) fn alloc_handle(&self) -> Option<FileHandle> {
for i in 0..MAX_OPEN_FILES {
if self.open_files[i].is_none() {
return Some(FileHandle::new(i as u16));
}
}
None
self.open_files.free().map(|i| FileHandle::new(i as u16))
}
pub(super) fn resolve_parent<'a>(&mut self, path: &'a [u8]) -> Option<(u32, &'a [u8])> {

View File

@@ -2,12 +2,12 @@
//! endpoint, so a badge it does not know (never issued, or released) is
//! no authority at all.
use alloc::vec;
use alloc::vec::Vec;
use types::bounded::CapSlot;
use crate::scope::Scope;
pub const MAX_GRANTS: usize = 16;
#[derive(Debug, Clone, Copy)]
struct Grant {
badge: usize,
@@ -20,7 +20,8 @@ struct Grant {
}
pub struct Grants {
grants: [Option<Grant>; MAX_GRANTS],
/// As many as storage's memory holds.
grants: Vec<Grant>,
/// Badges are never reused, so a released one stays dead.
next_badge: usize,
}
@@ -28,7 +29,7 @@ pub struct Grants {
impl Grants {
pub const fn new() -> Self {
Self {
grants: [None; MAX_GRANTS],
grants: Vec::new(),
next_badge: 1,
}
}
@@ -39,7 +40,6 @@ impl Grants {
b => self
.grants
.iter()
.flatten()
.find(|g| g.badge == b)
.map(|g| g.scope),
}
@@ -53,11 +53,10 @@ impl Grants {
scope: Scope,
mint: impl FnOnce(usize) -> Option<CapSlot>,
) -> Option<CapSlot> {
let free = self.grants.iter_mut().find(|g| g.is_none())?;
let badge = self.next_badge;
let cap = mint(badge)?;
self.next_badge += 1;
*free = Some(Grant {
self.grants.push(Grant {
badge,
scope,
parent,
@@ -66,14 +65,9 @@ impl Grants {
Some(cap)
}
/// The grants init made directly, into `out`; how many.
pub fn roots(&self, out: &mut [usize; MAX_GRANTS]) -> usize {
let mut n = 0;
for g in self.grants.iter().flatten().filter(|g| g.parent == 0) {
out[n] = g.badge;
n += 1;
}
n
/// The grants init made directly.
pub fn roots(&self) -> Vec<usize> {
self.grants.iter().filter(|g| g.parent == 0).map(|g| g.badge).collect()
}
/// Forgets `badge` and every grant narrowed from it, transitively,
@@ -82,24 +76,19 @@ impl Grants {
if badge == 0 {
return;
}
let mut doomed = [0usize; MAX_GRANTS];
doomed[0] = badge;
let mut n = 1;
let mut doomed = vec![badge];
let mut i = 0;
while i < n {
let b = doomed[i];
while let Some(&b) = doomed.get(i) {
i += 1;
for g in &mut self.grants {
let Some(grant) = g else { continue };
if grant.badge == b {
gone(grant.badge, grant.cap);
*g = None;
} else if grant.parent == b && n < MAX_GRANTS {
doomed[n] = grant.badge;
n += 1;
}
}
doomed.extend(self.grants.iter().filter(|g| g.parent == b).map(|g| g.badge));
}
self.grants.retain(|g| {
let dies = doomed.contains(&g.badge);
if dies {
gone(g.badge, g.cap);
}
!dies
});
}
}

View File

@@ -11,6 +11,7 @@ pub mod grants;
pub mod protocol;
pub mod scope;
pub mod server;
mod slots;
#[cfg(test)]
#[path = "tests/mod.rs"]

View File

@@ -62,6 +62,8 @@ pub enum StorageResponse {
/// Carries the new capability.
Granted,
Denied,
/// No connection free.
Full,
}
fn handle(word: usize) -> Option<FileHandle> {
@@ -143,6 +145,7 @@ impl Encode for StorageResponse {
NotAttached => [reply::NOT_ATTACHED, 0, 0, 0],
Granted => [reply::GRANTED, 0, 0, 0],
Denied => [reply::DENIED, 0, 0, 0],
Full => [reply::FULL, 0, 0, 0],
}
}
}
@@ -164,6 +167,7 @@ impl Decode for StorageResponse {
reply::NOT_ATTACHED => NotAttached,
reply::GRANTED => Granted,
reply::DENIED => Denied,
reply::FULL => Full,
_ => return None,
})
}

View File

@@ -10,9 +10,8 @@ use crate::scope::Scope;
mod session;
use types::storage::MAX_CLIENTS;
/// Handles at or past this are never valid (the file systems hand out
/// fewer), so ownership fits a small table.
const MAX_HANDLES: usize = 16;
use crate::slots::Slots;
const MAX_LISTED: usize = 64;
/// What the server needs from the kernel: regions for client buffers
@@ -50,7 +49,8 @@ pub struct StorageServer<F: FileSystem, H: Host> {
host: H,
grants: Grants,
clients: [Option<Client>; MAX_CLIENTS],
owners: [Option<Owner>; MAX_HANDLES],
/// Who opened each handle, by handle.
owners: Slots<Owner>,
log: Option<CapSlot>,
/// The master key's and the seed's halves, from the gatekeeper through
/// init; handed to the file system (and forgotten here) once all in.
@@ -68,7 +68,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
host,
grants: Grants::new(),
clients: [const { None }; MAX_CLIENTS],
owners: [None; MAX_HANDLES],
owners: Slots::new(),
log: None,
key: [None; 2],
seed: [None; 2],
@@ -144,7 +144,8 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
);
}
let Some(index) = self.clients.iter().position(Option::is_none) else {
return (StorageResponse::Error, None);
libkos::println!("storage: every connection ({}) is in use", MAX_CLIENTS);
return (StorageResponse::Full, None);
};
let Some((buf, cap)) = self.host.create(index) else {
return (StorageResponse::Error, None);
@@ -176,11 +177,10 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
} = self;
grants.release(badge, |gone, cap| {
host.revoke(cap);
for (raw, owner) in owners.iter_mut().enumerate() {
if owner.is_some_and(|o| o.badge == gone) {
fs.close(FileHandle::new(raw as u16));
*owner = None;
}
let theirs: alloc::vec::Vec<usize> = owners.iter().filter(|(_, o)| o.badge == gone).map(|(raw, _)| raw).collect();
for raw in theirs {
fs.close(FileHandle::new(raw as u16));
owners.take(raw);
}
for c in clients
.iter_mut()
@@ -198,7 +198,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
}
fn owner(&self, handle: FileHandle) -> Option<Owner> {
self.owners.get(handle.raw() as usize).copied().flatten()
self.owners.get(handle.raw() as usize).copied()
}
/// Serves one request from `badge`, which holds `scope`.
@@ -269,18 +269,11 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
resolved => resolved.flatten(),
};
let path = disk.as_ref().map_or(&[][..], |p| p.as_bytes());
let opened = |owners: &mut [Option<Owner>; MAX_HANDLES], h: Option<FileHandle>| match h {
Some(h) => match owners.get_mut(h.raw() as usize) {
Some(owner) => {
*owner = Some(Owner {
badge,
write: scope.write,
});
R::Handle(h)
}
// A handle the table cannot track is one we cannot police.
None => R::Error,
},
let opened = |owners: &mut Slots<Owner>, h: Option<FileHandle>| match h {
Some(h) => {
owners.put(h.raw() as usize, Owner { badge, write: scope.write });
R::Handle(h)
}
None => R::NotFound,
};
let response = match request {
@@ -311,9 +304,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
},
Close { handle } => {
fs.close(handle);
if let Some(owner) = owners.get_mut(handle.raw() as usize) {
*owner = None;
}
owners.take(handle.raw() as usize);
R::Ok
}
FileSize { handle } => match fs.file_size(handle) {

View File

@@ -6,7 +6,6 @@ use types::user::UserFolder;
use super::{Host, StorageServer};
use crate::filesystem::{FileHandle, FileSystem};
use crate::grants::MAX_GRANTS;
use crate::protocol::{MAX_PATH, StorageResponse};
impl<F: FileSystem, H: Host> StorageServer<F, H> {
@@ -46,15 +45,13 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
if badge != 0 {
return StorageResponse::Error;
}
let mut roots = [0usize; MAX_GRANTS];
let n = self.grants.roots(&mut roots);
for &root in &roots[..n] {
for root in self.grants.roots() {
self.release(root);
}
for (raw, owner) in self.owners.iter_mut().enumerate() {
if owner.take().is_some() {
self.fs.close(FileHandle::new(raw as u16));
}
let open: alloc::vec::Vec<usize> = self.owners.iter().map(|(raw, _)| raw).collect();
for raw in open {
self.owners.take(raw);
self.fs.close(FileHandle::new(raw as u16));
}
self.fs.lock();
self.key = [None; 2];

View File

@@ -0,0 +1,55 @@
//! A table that grows as it fills, bounded by storage's memory rather than
//! a count: an entry keeps its index for life (a handle names it), and a
//! freed index is reused first. At most what a `FileHandle` can name.
use alloc::vec::Vec;
pub const MAX: usize = u16::MAX as usize + 1;
pub struct Slots<T>(Vec<Option<T>>);
impl<T> Slots<T> {
pub const fn new() -> Self {
Self(Vec::new())
}
/// The index the next entry would get, if there is room for one.
pub fn free(&self) -> Option<usize> {
let i = self.0.iter().position(Option::is_none).unwrap_or(self.0.len());
(i < MAX).then_some(i)
}
/// Puts `value` at `index`, growing to it.
pub fn put(&mut self, index: usize, value: T) {
if index >= MAX {
return;
}
if self.0.len() <= index {
self.0.resize_with(index + 1, || None);
}
self.0[index] = Some(value);
}
pub fn get(&self, index: usize) -> Option<&T> {
self.0.get(index)?.as_ref()
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
self.0.get_mut(index)?.as_mut()
}
pub fn take(&mut self, index: usize) -> Option<T> {
self.0.get_mut(index)?.take()
}
/// Every entry, with its index.
pub fn iter(&self) -> impl Iterator<Item = (usize, &T)> {
self.0.iter().enumerate().filter_map(|(i, v)| v.as_ref().map(|v| (i, v)))
}
}
impl<T> Default for Slots<T> {
fn default() -> Self {
Self::new()
}
}

View File

@@ -595,3 +595,18 @@ fn delete_nonempty_dir_returns_false() {
let count = fs.list_dir(b"parent", &mut entries).unwrap();
assert_eq!(count, 1);
}
/// Open files were a table of 8: now as many as memory holds.
#[test]
fn many_files_open_at_once() {
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();
let handles: alloc::vec::Vec<_> = (0..40).map(|_| fs.open(b"hello").expect("opens")).collect();
let mut buf = [0u8; 32];
assert_eq!(fs.read(handles[39], &mut buf), Some(13));
for h in &handles[..20] {
fs.close(*h);
}
// Closed handles are used again.
let again = fs.open(b"hello").unwrap();
assert!(handles[..20].contains(&again));
}

View File

@@ -532,3 +532,32 @@ fn only_init_purges_only_users_and_only_while_locked() {
assert_eq!(purge(&mut s, 0, b"users/x"), StorageResponse::Ok);
assert_eq!(purge(&mut s, 0, b"users"), StorageResponse::Ok);
}
/// Grants were a table of 16, and releasing followed at most 16 narrowed
/// from one: neither is a limit any more.
#[test]
fn many_grants_and_all_narrowed_from_one_go_on_release() {
let mut s = server();
let root = Client::attach(&mut s, 0);
let siblings: Vec<usize> = (0..20).map(|_| root.grant(&mut s, b"", true)).collect();
let parent = Client::attach(&mut s, siblings[0]);
let children: Vec<usize> = (0..20).map(|_| parent.grant(&mut s, b"", false)).collect();
assert_eq!(call(&mut s, parent.badge, StorageRequest::Release).0, StorageResponse::Ok);
for badge in children {
assert_eq!(call(&mut s, badge, StorageRequest::Attach).0, StorageResponse::Denied, "child {badge}");
}
assert_eq!(call(&mut s, siblings[19], StorageRequest::Attach).0, StorageResponse::Attached { size: 4096 }, "a sibling stays");
}
/// Past the last connection, storage says it is full, not that it failed.
#[test]
fn a_connection_past_the_last_is_told_storage_is_full() {
let mut s = server();
let root = Client::attach(&mut s, 0);
let grants: Vec<usize> = (1..types::storage::MAX_CLIENTS).map(|_| root.grant(&mut s, b"", false)).collect();
for g in &grants {
Client::attach(&mut s, *g);
}
let one_more = root.grant(&mut s, b"", false);
assert_eq!(call(&mut s, one_more, StorageRequest::Attach).0, StorageResponse::Full);
}