use std::{
fmt,
fs::OpenOptions,
io::Error,
mem::zeroed,
os::{
fd::FromRawFd,
unix::{
fs::OpenOptionsExt,
io::{AsFd, AsRawFd, BorrowedFd},
},
},
path::Path,
};
use nix::{
fcntl::{open, OFlag},
sys::stat::Mode,
};
use serde::{Serialize, Serializer};
#[cfg(test)]
use strum::IntoEnumIterator;
#[cfg(test)]
use crate::landlock::{AccessError, RulesetAttr, RulesetCreatedAttr};
use crate::{
fd::SafeOwnedFd,
landlock::{
compat::private::OptionCompatLevelMut, uapi, Access, AddRuleError, AddRulesError,
CompatError, CompatLevel, CompatResult, CompatState, Compatible, HandleAccessError,
HandleAccessesError, PathBeneathError, PathFdError, PrivateAccess, PrivateRule, Rule,
Ruleset, RulesetCreated, RulesetError, TailoredCompatLevel, TryCompat, ABI,
},
lookup::{file_type, FileType},
};
crate::landlock::access::bitflags_type! {
pub struct AccessFs: u64 {
const Execute = uapi::LANDLOCK_ACCESS_FS_EXECUTE as u64;
const WriteFile = uapi::LANDLOCK_ACCESS_FS_WRITE_FILE as u64;
const ReadFile = uapi::LANDLOCK_ACCESS_FS_READ_FILE as u64;
const ReadDir = uapi::LANDLOCK_ACCESS_FS_READ_DIR as u64;
const RemoveDir = uapi::LANDLOCK_ACCESS_FS_REMOVE_DIR as u64;
const RemoveFile = uapi::LANDLOCK_ACCESS_FS_REMOVE_FILE as u64;
const MakeChar = uapi::LANDLOCK_ACCESS_FS_MAKE_CHAR as u64;
const MakeDir = uapi::LANDLOCK_ACCESS_FS_MAKE_DIR as u64;
const MakeReg = uapi::LANDLOCK_ACCESS_FS_MAKE_REG as u64;
const MakeSock = uapi::LANDLOCK_ACCESS_FS_MAKE_SOCK as u64;
const MakeFifo = uapi::LANDLOCK_ACCESS_FS_MAKE_FIFO as u64;
const MakeBlock = uapi::LANDLOCK_ACCESS_FS_MAKE_BLOCK as u64;
const MakeSym = uapi::LANDLOCK_ACCESS_FS_MAKE_SYM as u64;
const Refer = uapi::LANDLOCK_ACCESS_FS_REFER as u64;
const Truncate = uapi::LANDLOCK_ACCESS_FS_TRUNCATE as u64;
const IoctlDev = uapi::LANDLOCK_ACCESS_FS_IOCTL_DEV as u64;
}
}
impl TailoredCompatLevel for AccessFs {}
impl Access for AccessFs {
fn from_all(abi: ABI) -> Self {
Self::from_read(abi) | Self::from_write(abi)
}
}
impl AccessFs {
pub fn from_read(abi: ABI) -> Self {
match abi {
ABI::Unsupported => AccessFs::EMPTY,
ABI::V1 | ABI::V2 | ABI::V3 | ABI::V4 | ABI::V5 | ABI::V6 | ABI::V7 | ABI::V8 => {
make_bitflags!(AccessFs::{
Execute
| ReadFile
| ReadDir
})
}
}
}
pub fn from_write(abi: ABI) -> Self {
match abi {
ABI::Unsupported => AccessFs::EMPTY,
ABI::V1 => make_bitflags!(AccessFs::{
WriteFile
| RemoveDir
| RemoveFile
| MakeChar
| MakeDir
| MakeReg
| MakeSock
| MakeFifo
| MakeBlock
| MakeSym
}),
ABI::V2 => Self::from_write(ABI::V1) | AccessFs::Refer,
ABI::V3 | ABI::V4 => Self::from_write(ABI::V2) | AccessFs::Truncate,
ABI::V5 | ABI::V6 | ABI::V7 | ABI::V8 => Self::from_write(ABI::V4) | AccessFs::IoctlDev,
}
}
pub fn from_file(abi: ABI) -> Self {
Self::from_all(abi) & ACCESS_FILE
}
}
#[test]
fn consistent_access_fs_rw() {
for abi in ABI::iter() {
let access_all = AccessFs::from_all(abi);
let access_read = AccessFs::from_read(abi);
let access_write = AccessFs::from_write(abi);
let access_file = AccessFs::from_file(abi);
assert_eq!(access_read, !access_write & access_all);
assert_eq!(access_read | access_write, access_all);
assert_eq!(access_file, access_all & ACCESS_FILE);
}
}
impl PrivateAccess for AccessFs {
fn is_empty(self) -> bool {
AccessFs::is_empty(&self)
}
fn ruleset_handle_access(
ruleset: &mut Ruleset,
access: Self,
) -> Result<(), HandleAccessesError> {
ruleset.requested_handled_fs |= access;
ruleset.actual_handled_fs |= match access
.try_compat(
ruleset.compat.abi(),
ruleset.compat.level,
&mut ruleset.compat.state,
)
.map_err(HandleAccessError::Compat)?
{
Some(a) => a,
None => return Ok(()),
};
Ok(())
}
fn into_add_rules_error(error: AddRuleError<Self>) -> AddRulesError {
AddRulesError::Fs(error)
}
fn into_handle_accesses_error(error: HandleAccessError<Self>) -> HandleAccessesError {
HandleAccessesError::Fs(error)
}
}
impl fmt::Display for AccessFs {
#[expect(clippy::cognitive_complexity)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts = Vec::new();
if self.contains(AccessFs::ReadFile) {
parts.push("read");
}
if self.contains(AccessFs::WriteFile) {
parts.push("write");
}
if self.contains(AccessFs::Execute) {
parts.push("exec");
}
if self.contains(AccessFs::IoctlDev) {
parts.push("ioctl");
}
if self.contains(AccessFs::MakeReg) {
parts.push("create");
}
if self.contains(AccessFs::RemoveFile) {
parts.push("delete");
}
if self.contains(AccessFs::Refer) {
parts.push("rename");
}
if self.contains(AccessFs::MakeSym) {
parts.push("symlink");
}
if self.contains(AccessFs::Truncate) {
parts.push("truncate");
}
if self.contains(AccessFs::ReadDir) {
parts.push("readdir");
}
if self.contains(AccessFs::MakeDir) {
parts.push("mkdir");
}
if self.contains(AccessFs::RemoveDir) {
parts.push("rmdir");
}
if self.contains(AccessFs::MakeChar) {
parts.push("mkdev");
}
if self.contains(AccessFs::MakeFifo) {
parts.push("mkfifo");
}
if self.contains(AccessFs::MakeSock) {
parts.push("bind");
}
write!(f, "{}", parts.join(", "))
}
}
impl Serialize for AccessFs {
#[expect(clippy::cognitive_complexity)]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut items = Vec::new();
if self.contains(Self::ReadFile) {
items.push("read");
}
if self.contains(Self::WriteFile) {
items.push("write");
}
if self.contains(Self::Execute) {
items.push("exec");
}
if self.contains(Self::IoctlDev) {
items.push("ioctl");
}
if self.contains(Self::MakeReg) {
items.push("create");
}
if self.contains(Self::RemoveFile) {
items.push("delete");
}
if self.contains(Self::Refer) {
items.push("rename");
}
if self.contains(Self::MakeSym) {
items.push("symlink");
}
if self.contains(Self::Truncate) {
items.push("truncate");
}
if self.contains(Self::ReadDir) {
items.push("readdir");
}
if self.contains(Self::MakeDir) {
items.push("mkdir");
}
if self.contains(Self::RemoveDir) {
items.push("rmdir");
}
if self.contains(Self::MakeChar) {
items.push("mkdev");
}
if self.contains(Self::MakeFifo) {
items.push("mkfifo");
}
if self.contains(Self::MakeSock) {
items.push("bind");
}
items.serialize(serializer)
}
}
const ACCESS_FILE: AccessFs = make_bitflags!(AccessFs::{
ReadFile | WriteFile | Execute | Truncate | IoctlDev
});
fn is_file<F>(fd: F) -> Result<bool, Error>
where
F: AsFd,
{
file_type(fd, None, false)
.map(|typ| typ != FileType::Dir)
.map_err(|errno| Error::from_raw_os_error(errno as i32))
}
#[derive(Debug)]
pub struct PathBeneath<F> {
attr: uapi::landlock_path_beneath_attr,
parent_fd: F,
allowed_access: AccessFs,
compat_level: Option<CompatLevel>,
}
impl<F> PathBeneath<F>
where
F: AsFd,
{
pub fn new<A>(parent: F, access: A) -> Self
where
A: Into<AccessFs>,
{
PathBeneath {
attr: unsafe { zeroed() },
parent_fd: parent,
allowed_access: access.into(),
compat_level: None,
}
}
}
impl<F> TryCompat<AccessFs> for PathBeneath<F>
where
F: AsFd,
{
fn try_compat_children<L>(
mut self,
abi: ABI,
parent_level: L,
compat_state: &mut CompatState,
) -> Result<Option<Self>, CompatError<AccessFs>>
where
L: Into<CompatLevel>,
{
self.allowed_access = match self.allowed_access.try_compat(
abi,
self.tailored_compat_level(parent_level),
compat_state,
)? {
Some(a) => a,
None => return Ok(None),
};
Ok(Some(self))
}
fn try_compat_inner(
&mut self,
_abi: ABI,
) -> Result<CompatResult<AccessFs>, CompatError<AccessFs>> {
let valid_access =
if is_file(&self.parent_fd).map_err(|e| PathBeneathError::StatCall { source: e })? {
self.allowed_access & ACCESS_FILE
} else {
self.allowed_access
};
if self.allowed_access != valid_access {
let error = PathBeneathError::DirectoryAccess {
access: self.allowed_access,
incompatible: self.allowed_access ^ valid_access,
}
.into();
self.allowed_access = valid_access;
Ok(CompatResult::Partial(error))
} else {
Ok(CompatResult::Full)
}
}
}
#[test]
fn path_beneath_try_compat_children() {
use crate::*;
let access_file = AccessFs::ReadFile | AccessFs::Refer;
let mut ruleset = Ruleset::from(ABI::V1).handle_access(access_file).unwrap();
ruleset.compat.state = CompatState::Dummy;
assert!(matches!(
RulesetCreated::new(ruleset, None)
.set_compatibility(CompatLevel::HardRequirement)
.add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file))
.unwrap_err(),
RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat(
CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
))) if access == access_file && incompatible == AccessFs::Refer
));
let mut ruleset = Ruleset::from(ABI::V2).handle_access(access_file).unwrap();
ruleset.compat.state = CompatState::Dummy;
assert!(matches!(
RulesetCreated::new(ruleset, None)
.set_compatibility(CompatLevel::HardRequirement)
.add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file))
.unwrap_err(),
RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat(
CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
))) if access == access_file && incompatible == AccessFs::Refer
));
}
#[test]
fn path_beneath_try_compat() {
use crate::*;
let abi = ABI::V1;
for file in &["/etc/passwd", "/dev/null"] {
let mut compat_state = CompatState::Init;
let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
assert!(matches!(
PathBeneath::new(PathFd::new(file).unwrap(), ro_access)
.try_compat(abi, CompatLevel::HardRequirement, &mut compat_state)
.unwrap_err(),
CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
if access == ro_access && incompatible == AccessFs::ReadDir
));
let mut compat_state = CompatState::Init;
assert!(matches!(
PathBeneath::new(PathFd::new(file).unwrap(), AccessFs::EMPTY)
.try_compat(abi, CompatLevel::BestEffort, &mut compat_state)
.unwrap_err(),
CompatError::Access(AccessError::Empty)
));
}
let full_access = AccessFs::from_all(ABI::V1);
for compat_level in &[
CompatLevel::BestEffort,
CompatLevel::SoftRequirement,
CompatLevel::HardRequirement,
] {
let mut compat_state = CompatState::Init;
let mut path_beneath = PathBeneath::new(PathFd::new("/").unwrap(), full_access)
.try_compat(abi, *compat_level, &mut compat_state)
.unwrap()
.unwrap();
assert_eq!(compat_state, CompatState::Full);
let raw_access = path_beneath.attr.allowed_access;
assert_eq!(raw_access, 0);
let _ = path_beneath.as_ptr();
let raw_access = path_beneath.attr.allowed_access;
assert_eq!(raw_access, full_access.bits());
}
}
impl<F> OptionCompatLevelMut for PathBeneath<F> {
fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
&mut self.compat_level
}
}
impl<F> OptionCompatLevelMut for &mut PathBeneath<F> {
fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
&mut self.compat_level
}
}
impl<F> Compatible for PathBeneath<F> {}
impl<F> Compatible for &mut PathBeneath<F> {}
#[test]
fn path_beneath_compatibility() {
let mut path = PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::from_all(ABI::V1));
let path_ref = &mut path;
let level = path_ref.as_option_compat_level_mut();
assert_eq!(level, &None);
assert_eq!(
<Option<CompatLevel> as Into<CompatLevel>>::into(*level),
CompatLevel::BestEffort
);
path_ref.set_compatibility(CompatLevel::SoftRequirement);
assert_eq!(
path_ref.as_option_compat_level_mut(),
&Some(CompatLevel::SoftRequirement)
);
path.set_compatibility(CompatLevel::HardRequirement);
}
impl<F> Rule<AccessFs> for PathBeneath<F> where F: AsFd {}
impl<F> PrivateRule<AccessFs> for PathBeneath<F>
where
F: AsFd,
{
const TYPE_ID: uapi::landlock_rule_type = uapi::landlock_rule_type_LANDLOCK_RULE_PATH_BENEATH;
fn as_ptr(&mut self) -> *const libc::c_void {
self.attr.parent_fd = self.parent_fd.as_fd().as_raw_fd();
self.attr.allowed_access = self.allowed_access.bits();
&self.attr as *const _ as _
}
fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError> {
if ruleset.requested_handled_fs.contains(self.allowed_access) {
Ok(())
} else {
Err(AddRuleError::UnhandledAccess {
access: self.allowed_access,
incompatible: self.allowed_access & !ruleset.requested_handled_fs,
}
.into())
}
}
}
#[test]
fn path_beneath_check_consistency() {
use crate::*;
let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
let rx_access = AccessFs::Execute | AccessFs::ReadFile;
assert!(matches!(
Ruleset::from(ABI::Unsupported)
.handle_access(ro_access)
.unwrap()
.create()
.unwrap()
.add_rule(PathBeneath::new(PathFd::new("/").unwrap(), rx_access))
.unwrap_err(),
RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { access, incompatible }))
if access == rx_access && incompatible == AccessFs::Execute
));
}
#[derive(Debug)]
pub struct PathFd {
pub fd: SafeOwnedFd,
}
impl PathFd {
pub fn new<T>(path: T) -> Result<Self, PathFdError>
where
T: AsRef<Path>,
{
Ok(PathFd {
fd: open(
path.as_ref(),
OFlag::O_PATH | OFlag::O_CLOEXEC,
Mode::empty(),
)
.map_err(|e| PathFdError::OpenCall {
source: Error::from_raw_os_error(e as i32),
path: path.as_ref().into(),
})?
.into(),
})
}
}
impl AsFd for PathFd {
fn as_fd(&self) -> BorrowedFd<'_> {
self.fd.as_fd()
}
}
#[test]
fn path_fd() {
use std::{fs::File, io::Read};
PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Execute);
if let Ok(file) = File::open("/") {
PathBeneath::new(file, AccessFs::Execute);
}
let mut buffer = [0; 1];
File::from(PathFd::new("/etc/passwd").unwrap().fd)
.read(&mut buffer)
.unwrap_err();
}
pub fn path_beneath_rules<I, P>(
paths: I,
access: AccessFs,
) -> impl Iterator<Item = Result<PathBeneath<PathFd>, RulesetError>>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
paths.into_iter().filter_map(move |p| match PathFd::new(p) {
Ok(f) => {
let valid_access = match is_file(&f) {
Ok(true) => access & ACCESS_FILE,
Err(_) | Ok(false) => access,
};
Some(Ok(PathBeneath::new(f, valid_access)))
}
Err(_) => None,
})
}
#[test]
fn path_beneath_rules_iter() {
let _ = Ruleset::default()
.handle_access(AccessFs::from_all(ABI::V1))
.unwrap()
.create()
.unwrap()
.add_rules(path_beneath_rules(
&["/usr", "/opt", "/does-not-exist", "/root"],
AccessFs::Execute,
))
.unwrap();
}
#[test]
fn test_display_single_flags_0() {
assert_eq!(format!("{}", AccessFs::ReadFile), "read");
assert_eq!(format!("{}", AccessFs::WriteFile), "write");
assert_eq!(format!("{}", AccessFs::Execute), "exec");
assert_eq!(format!("{}", AccessFs::IoctlDev), "ioctl");
assert_eq!(format!("{}", AccessFs::MakeReg), "create");
assert_eq!(format!("{}", AccessFs::RemoveFile), "delete");
assert_eq!(format!("{}", AccessFs::Refer), "rename");
assert_eq!(format!("{}", AccessFs::MakeSym), "symlink");
assert_eq!(format!("{}", AccessFs::Truncate), "truncate");
assert_eq!(format!("{}", AccessFs::ReadDir), "readdir");
assert_eq!(format!("{}", AccessFs::MakeDir), "mkdir");
assert_eq!(format!("{}", AccessFs::RemoveDir), "rmdir");
assert_eq!(format!("{}", AccessFs::MakeChar), "mkdev");
assert_eq!(format!("{}", AccessFs::MakeFifo), "mkfifo");
assert_eq!(format!("{}", AccessFs::MakeSock), "bind");
}
#[test]
fn test_display_combined_flags_0() {
let access = AccessFs::ReadFile | AccessFs::WriteFile;
assert_eq!(format!("{access}"), "read, write");
}
#[test]
fn test_display_combined_flags_1() {
let access = AccessFs::Execute | AccessFs::ReadDir | AccessFs::MakeDir;
assert_eq!(format!("{access}"), "exec, readdir, mkdir");
}
#[test]
fn test_display_empty_0() {
assert_eq!(format!("{}", AccessFs::EMPTY), "");
}
#[test]
fn test_serialize_single_flags_0() {
let access = AccessFs::ReadFile;
let json = serde_json::to_string(&access).unwrap();
assert_eq!(json, r#"["read"]"#);
}
#[test]
fn test_serialize_combined_flags_0() {
let access = AccessFs::ReadFile | AccessFs::Execute;
let json = serde_json::to_string(&access).unwrap();
assert_eq!(json, r#"["read","exec"]"#);
}
#[test]
fn test_serialize_empty_0() {
let json = serde_json::to_string(&AccessFs::EMPTY).unwrap();
assert_eq!(json, "[]");
}
#[test]
fn test_from_read_unsupported_0() {
assert_eq!(AccessFs::from_read(ABI::Unsupported), AccessFs::EMPTY);
}
#[test]
fn test_from_write_unsupported_0() {
assert_eq!(AccessFs::from_write(ABI::Unsupported), AccessFs::EMPTY);
}
#[test]
fn test_from_all_unsupported_0() {
assert_eq!(AccessFs::from_all(ABI::Unsupported), AccessFs::EMPTY);
}
#[test]
fn test_from_file_unsupported_0() {
assert_eq!(AccessFs::from_file(ABI::Unsupported), AccessFs::EMPTY);
}
#[test]
fn test_from_read_v1_0() {
let read = AccessFs::from_read(ABI::V1);
assert!(read.contains(AccessFs::Execute));
assert!(read.contains(AccessFs::ReadFile));
assert!(read.contains(AccessFs::ReadDir));
assert!(!read.contains(AccessFs::WriteFile));
}
#[test]
fn test_from_write_v1_0() {
let write = AccessFs::from_write(ABI::V1);
assert!(write.contains(AccessFs::WriteFile));
assert!(write.contains(AccessFs::RemoveDir));
assert!(write.contains(AccessFs::MakeSym));
assert!(!write.contains(AccessFs::Execute));
assert!(!write.contains(AccessFs::Refer));
}
#[test]
fn test_from_write_v2_0() {
let write = AccessFs::from_write(ABI::V2);
assert!(write.contains(AccessFs::Refer));
assert!(!write.contains(AccessFs::Truncate));
}
#[test]
fn test_from_write_v3_0() {
let write = AccessFs::from_write(ABI::V3);
assert!(write.contains(AccessFs::Truncate));
assert!(write.contains(AccessFs::Refer));
assert!(!write.contains(AccessFs::IoctlDev));
}
#[test]
fn test_from_write_v5_0() {
let write = AccessFs::from_write(ABI::V5);
assert!(write.contains(AccessFs::IoctlDev));
assert!(write.contains(AccessFs::Truncate));
}
#[test]
fn test_from_file_0() {
let file_v1 = AccessFs::from_file(ABI::V1);
assert!(file_v1.contains(AccessFs::ReadFile));
assert!(file_v1.contains(AccessFs::WriteFile));
assert!(file_v1.contains(AccessFs::Execute));
assert!(!file_v1.contains(AccessFs::ReadDir));
assert!(!file_v1.contains(AccessFs::MakeDir));
}
#[test]
fn test_access_file_contents_0() {
assert!(ACCESS_FILE.contains(AccessFs::ReadFile));
assert!(ACCESS_FILE.contains(AccessFs::WriteFile));
assert!(ACCESS_FILE.contains(AccessFs::Execute));
assert!(ACCESS_FILE.contains(AccessFs::Truncate));
assert!(ACCESS_FILE.contains(AccessFs::IoctlDev));
assert!(!ACCESS_FILE.contains(AccessFs::ReadDir));
assert!(!ACCESS_FILE.contains(AccessFs::MakeDir));
assert!(!ACCESS_FILE.contains(AccessFs::RemoveDir));
assert!(!ACCESS_FILE.contains(AccessFs::MakeChar));
assert!(!ACCESS_FILE.contains(AccessFs::MakeSock));
assert!(!ACCESS_FILE.contains(AccessFs::MakeFifo));
assert!(!ACCESS_FILE.contains(AccessFs::MakeBlock));
assert!(!ACCESS_FILE.contains(AccessFs::MakeSym));
assert!(!ACCESS_FILE.contains(AccessFs::Refer));
assert!(!ACCESS_FILE.contains(AccessFs::MakeReg));
assert!(!ACCESS_FILE.contains(AccessFs::RemoveFile));
}
#[test]
fn test_is_empty_0() {
use crate::landlock::PrivateAccess;
assert!(PrivateAccess::is_empty(AccessFs::EMPTY));
assert!(!PrivateAccess::is_empty(AccessFs::Execute));
}
#[test]
fn test_path_fd_nonexistent_0() {
assert!(PathFd::new("/nonexistent/path/does/not/exist").is_err());
}
#[test]
fn test_path_fd_error_display_0() {
let err = PathFd::new("/nonexistent/path").unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("/nonexistent/path"));
assert!(msg.contains("failed to open"));
}
#[test]
fn test_path_beneath_new_0() {
let fd = PathFd::new("/").unwrap();
let pb = PathBeneath::new(fd, AccessFs::Execute);
assert_eq!(pb.allowed_access, AccessFs::Execute);
assert_eq!(pb.compat_level, None);
}
#[test]
fn test_from_all_monotonic_0() {
let mut prev = AccessFs::EMPTY;
for abi in ABI::iter() {
let current = AccessFs::from_all(abi);
assert!(
current.contains(prev),
"ABI {abi:?} should be superset of previous"
);
prev = current;
}
}