Commit
This syscall is a complement to pledge() and adds the same sort of incremental relinquishing of capabilities for filesystem access. The first call to unveil() will "drop a veil" on the process, and from now on, only unveiled parts of the filesystem are visible to it. Each call to unveil() specifies a path to either a directory or a file along with permissions for that path. The permissions are a combination of the following: - r: Read access (like the "rpath" promise) - w: Write access (like the "wpath" promise) - x: Execute access - c: Create/remove access (like the "cpath" promise) Attempts to open a path that has not been unveiled with fail with ENOENT. If the unveiled path lacks sufficient permissions, it will fail with EACCES. Like pledge(), subsequent calls to unveil() with the same path can only remove permissions, not add them. Once you call unveil(nullptr, nullptr), the veil is locked, and it's no longer possible to unveil any more paths for the process, ever. This concept comes from OpenBSD, and their implementation does various things differently, I'm sure. This is just a first implementation for SerenityOS, and we'll keep improving on it as we go. :^)
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -564,7 +564,7 @@ KResult VFS::link(StringView old_path, StringView new_path, Custody& base) | |
KResult VFS::unlink(StringView path, Custody& base) | ||
{ | ||
RefPtr<Custody> parent_custody; | ||
auto custody_or_error = resolve_path(path, base, &parent_custody, O_NOFOLLOW_NOERROR); | ||
auto custody_or_error = resolve_path(path, base, &parent_custody, O_NOFOLLOW_NOERROR | O_UNLINK_INTERNAL); | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
awesomekling
Author
Member
|
||
if (custody_or_error.is_error()) | ||
return custody_or_error.error(); | ||
auto& custody = *custody_or_error.value(); | ||
|
@@ -709,8 +709,70 @@ Custody& VFS::root_custody() | |
return *m_root_custody; | ||
} | ||
|
||
const UnveiledPath* VFS::find_matching_unveiled_path(StringView path) | ||
{ | ||
for (auto& unveiled_path : current->process().unveiled_paths()) { | ||
if (path == unveiled_path.path) | ||
return &unveiled_path; | ||
if (path.starts_with(unveiled_path.path) && path.length() > unveiled_path.path.length() && path[unveiled_path.path.length()] == '/') | ||
return &unveiled_path; | ||
} | ||
return nullptr; | ||
} | ||
|
||
KResult VFS::validate_path_against_process_veil(StringView path, int options) | ||
{ | ||
if (current->process().unveil_state() == UnveilState::None) | ||
return KSuccess; | ||
|
||
// FIXME: Figure out a nicer way to do this. | ||
if (String(path).contains("/..")) | ||
return KResult(-EINVAL); | ||
|
||
auto* unveiled_path = find_matching_unveiled_path(path); | ||
if (!unveiled_path) { | ||
dbg() << *current << " rejecting path '" << path << "' since it hasn't been unveiled."; | ||
return KResult(-ENOENT); | ||
} | ||
|
||
if (options & O_CREAT) { | ||
if (!(unveiled_path->permissions & UnveiledPath::Access::CreateOrRemove)) { | ||
dbg() << *current << " rejecting path '" << path << "' since it hasn't been unveiled with 'c' permission."; | ||
return KResult(-EACCES); | ||
} | ||
} | ||
if (options & O_UNLINK_INTERNAL) { | ||
if (!(unveiled_path->permissions & UnveiledPath::Access::CreateOrRemove)) { | ||
dbg() << *current << " rejecting path '" << path << "' for unlink since it hasn't been unveiled with 'c' permission."; | ||
return KResult(-EACCES); | ||
} | ||
return KSuccess; | ||
} | ||
if ((options & O_RDWR) || (options & O_WRONLY)) { | ||
if (!(unveiled_path->permissions & UnveiledPath::Access::Write)) { | ||
dbg() << *current << " rejecting path '" << path << "' since it hasn't been unveiled with 'w' permission."; | ||
return KResult(-EACCES); | ||
} | ||
} else if (options & O_EXEC) { | ||
if (!(unveiled_path->permissions & UnveiledPath::Access::Execute)) { | ||
dbg() << *current << " rejecting path '" << path << "' since it hasn't been unveiled with 'x' permission."; | ||
return KResult(-EACCES); | ||
} | ||
} else { | ||
if (!(unveiled_path->permissions & UnveiledPath::Access::Read)) { | ||
dbg() << *current << " rejecting path '" << path << "' since it hasn't been unveiled with 'r' permission."; | ||
return KResult(-EACCES); | ||
} | ||
} | ||
return KSuccess; | ||
} | ||
|
||
KResultOr<NonnullRefPtr<Custody>> VFS::resolve_path(StringView path, Custody& base, RefPtr<Custody>* out_parent, int options, int symlink_recursion_level) | ||
{ | ||
auto result = validate_path_against_process_veil(path, options); | ||
if (result.is_error()) | ||
return result; | ||
|
||
if (symlink_recursion_level >= symlink_recursion_limit) | ||
return KResult(-ELOOP); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1820,6 +1820,10 @@ bool Process::validate(const Syscall::ImmutableBufferArgument<DataType, SizeType | |
|
||
String Process::validate_and_copy_string_from_user(const char* user_characters, size_t user_length) const | ||
{ | ||
if (!user_characters) | ||
return {}; | ||
if (user_length == 0) | ||
return String::empty(); | ||
if (!validate_read(user_characters, user_length)) | ||
return {}; | ||
SmapDisabler disabler; | ||
|
@@ -1933,6 +1937,9 @@ int Process::sys$open(const Syscall::SC_open_params* user_params) | |
if (options & O_NOFOLLOW_NOERROR) | ||
return -EINVAL; | ||
|
||
if (options & O_UNLINK_INTERNAL) | ||
return -EINVAL; | ||
|
||
if ((options & O_RDWR) || (options & O_WRONLY)) | ||
REQUIRE_PROMISE(wpath); | ||
else | ||
|
@@ -4588,3 +4595,74 @@ Region& Process::add_region(NonnullOwnPtr<Region> region) | |
m_regions.append(move(region)); | ||
return *ptr; | ||
} | ||
|
||
int Process::sys$unveil(const Syscall::SC_unveil_params* user_params) | ||
{ | ||
Syscall::SC_unveil_params params; | ||
if (!validate_read_and_copy_typed(¶ms, user_params)) | ||
return -EFAULT; | ||
|
||
if (!params.path.characters && !params.permissions.characters) { | ||
m_unveil_state = UnveilState::VeilLocked; | ||
return 0; | ||
} | ||
|
||
if (m_unveil_state == UnveilState::VeilLocked) | ||
return -EPERM; | ||
|
||
if (!params.path.characters || !params.permissions.characters) | ||
return -EINVAL; | ||
|
||
if (params.permissions.length > 4) | ||
return -EINVAL; | ||
|
||
auto path = get_syscall_path_argument(params.path); | ||
if (path.is_error()) | ||
return path.error(); | ||
|
||
if (path.value().is_empty() || path.value().characters()[0] != '/') | ||
return -EINVAL; | ||
|
||
auto permissions = validate_and_copy_string_from_user(params.permissions); | ||
if (permissions.is_null()) | ||
return -EFAULT; | ||
|
||
unsigned new_permissions = 0; | ||
for (size_t i = 0; i < permissions.length(); ++i) { | ||
switch (permissions[i]) { | ||
case 'r': | ||
new_permissions |= UnveiledPath::Access::Read; | ||
break; | ||
case 'w': | ||
new_permissions |= UnveiledPath::Access::Write; | ||
break; | ||
case 'x': | ||
new_permissions |= UnveiledPath::Access::Execute; | ||
break; | ||
case 'c': | ||
new_permissions |= UnveiledPath::Access::CreateOrRemove; | ||
break; | ||
default: | ||
return -EINVAL; | ||
} | ||
} | ||
|
||
for (int i = 0; i < m_unveiled_paths.size(); ++i) { | ||
auto& unveiled_path = m_unveiled_paths[i]; | ||
if (unveiled_path.path == path.value()) { | ||
if (new_permissions & ~unveiled_path.permissions) | ||
return -EPERM; | ||
if (!new_permissions) { | ||
m_unveiled_paths.remove(i); | ||
This comment has been minimized.
Sorry, something went wrong.
bugaevc
Member
|
||
return 0; | ||
} | ||
unveiled_path.permissions = new_permissions; | ||
return 0; | ||
} | ||
} | ||
|
||
m_unveiled_paths.append({ path.value(), new_permissions }); | ||
ASSERT(m_unveil_state != UnveilState::VeilLocked); | ||
m_unveil_state = UnveilState::VeilDropped; | ||
return 0; | ||
} |
2 comments
on commit 0569123
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also shouldn't this preserved at least across fork()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also shouldn't this preserved at least across
fork()
?
Definitely.
It's kinda strange to introduce a flag for one of the many places that uses
resolve_path()
, don't you think? Or do all other call sites pass appropriate options already? (Didn't look that way to me last time I checked...)