Kernel+LibCore+LibC: Implement support for forcing unveil on exec

To accomplish this, we add another VeilState which is called
LockedInherited. The idea is to apply exec unveil data, similar to
execpromises of the pledge syscall, on the current exec'ed program
during the execve sequence. When applying the forced unveil data, the
veil state is set to be locked but the special state of LockedInherited
ensures that if the new program tries to unveil paths, the request will
silently be ignored, so the program will continue running without
receiving an error, but is still can only use the paths that were
unveiled before the exec syscall. This in turn, allows us to use the
unveil syscall with a special utility to sandbox other userland programs
in terms of what is visible to them on the filesystem, and is usable on
both programs that use or don't use the unveil syscall in their code.
This commit is contained in:
Liav A 2022-11-04 19:20:11 +02:00 committed by Andrew Kaster
parent 35efdb17f9
commit 718ae68621
11 changed files with 161 additions and 48 deletions

View file

@ -433,6 +433,7 @@ struct SC_pledge_params {
}; };
struct SC_unveil_params { struct SC_unveil_params {
int flags;
StringArgument path; StringArgument path;
StringArgument permissions; StringArgument permissions;
}; };

22
Kernel/API/Unveil.h Normal file
View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2022, Liav A. <liavalb@hotmail.co.il>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/EnumBits.h>
#include <AK/Types.h>
namespace Kernel {
enum class UnveilFlags : u32 {
None = 0,
CurrentProgram = 1 << 0,
AfterExec = 1 << 1,
};
AK_ENUM_BITWISE_OPERATORS(UnveilFlags);
}

View file

@ -53,6 +53,11 @@ ErrorOr<void> SysFSOverallProcesses::try_generate(KBufferBuilder& builder)
case VeilState::Locked: case VeilState::Locked:
TRY(process_object.add("veil"sv, "Locked")); TRY(process_object.add("veil"sv, "Locked"));
break; break;
case VeilState::LockedInherited:
// Note: We don't reveal if the locked state is either by our choice
// or someone else applied it.
TRY(process_object.add("veil"sv, "Locked"));
break;
} }
} else { } else {
TRY(process_object.add("pledge"sv, ""sv)); TRY(process_object.add("pledge"sv, ""sv));

View file

@ -320,13 +320,14 @@ ErrorOr<NonnullLockRefPtr<Process>> Process::try_create(LockRefPtr<Thread>& firs
new_address_space = TRY(Memory::AddressSpace::try_create(nullptr)); new_address_space = TRY(Memory::AddressSpace::try_create(nullptr));
} }
auto unveil_tree = UnveilNode { TRY(KString::try_create("/"sv)), UnveilMetadata(TRY(KString::try_create("/"sv))) }; auto unveil_tree = UnveilNode { TRY(KString::try_create("/"sv)), UnveilMetadata(TRY(KString::try_create("/"sv))) };
auto exec_unveil_tree = UnveilNode { TRY(KString::try_create("/"sv)), UnveilMetadata(TRY(KString::try_create("/"sv))) };
auto credentials = TRY(Credentials::create(uid, gid, uid, gid, uid, gid, {})); auto credentials = TRY(Credentials::create(uid, gid, uid, gid, uid, gid, {}));
auto process = TRY(adopt_nonnull_lock_ref_or_enomem(new (nothrow) Process(move(name), move(credentials), ppid, is_kernel_process, move(current_directory), move(executable), tty, move(unveil_tree)))); auto process = TRY(adopt_nonnull_lock_ref_or_enomem(new (nothrow) Process(move(name), move(credentials), ppid, is_kernel_process, move(current_directory), move(executable), tty, move(unveil_tree), move(exec_unveil_tree))));
TRY(process->attach_resources(new_address_space.release_nonnull(), first_thread, fork_parent)); TRY(process->attach_resources(new_address_space.release_nonnull(), first_thread, fork_parent));
return process; return process;
} }
Process::Process(NonnullOwnPtr<KString> name, NonnullRefPtr<Credentials> credentials, ProcessID ppid, bool is_kernel_process, RefPtr<Custody> current_directory, RefPtr<Custody> executable, TTY* tty, UnveilNode unveil_tree) Process::Process(NonnullOwnPtr<KString> name, NonnullRefPtr<Credentials> credentials, ProcessID ppid, bool is_kernel_process, RefPtr<Custody> current_directory, RefPtr<Custody> executable, TTY* tty, UnveilNode unveil_tree, UnveilNode exec_unveil_tree)
: m_name(move(name)) : m_name(move(name))
, m_space(LockRank::None) , m_space(LockRank::None)
, m_protected_data_lock(LockRank::None) , m_protected_data_lock(LockRank::None)
@ -335,6 +336,7 @@ Process::Process(NonnullOwnPtr<KString> name, NonnullRefPtr<Credentials> credent
, m_current_directory(LockRank::None, move(current_directory)) , m_current_directory(LockRank::None, move(current_directory))
, m_tty(tty) , m_tty(tty)
, m_unveil_data(LockRank::None, move(unveil_tree)) , m_unveil_data(LockRank::None, move(unveil_tree))
, m_exec_unveil_data(LockRank::None, move(exec_unveil_tree))
, m_wait_blocker_set(*this) , m_wait_blocker_set(*this)
{ {
// Ensure that we protect the process data when exiting the constructor. // Ensure that we protect the process data when exiting the constructor.

View file

@ -84,6 +84,7 @@ enum class VeilState {
None, None,
Dropped, Dropped,
Locked, Locked,
LockedInherited,
}; };
static constexpr FlatPtr futex_key_private_flag = 0b1; static constexpr FlatPtr futex_key_private_flag = 0b1;
@ -523,6 +524,9 @@ public:
auto& unveil_data() { return m_unveil_data; } auto& unveil_data() { return m_unveil_data; }
auto const& unveil_data() const { return m_unveil_data; } auto const& unveil_data() const { return m_unveil_data; }
auto& exec_unveil_data() { return m_exec_unveil_data; }
auto const& exec_unveil_data() const { return m_exec_unveil_data; }
bool wait_for_tracer_at_next_execve() const bool wait_for_tracer_at_next_execve() const
{ {
return m_wait_for_tracer_at_next_execve; return m_wait_for_tracer_at_next_execve;
@ -584,7 +588,7 @@ private:
bool add_thread(Thread&); bool add_thread(Thread&);
bool remove_thread(Thread&); bool remove_thread(Thread&);
Process(NonnullOwnPtr<KString> name, NonnullRefPtr<Credentials>, ProcessID ppid, bool is_kernel_process, RefPtr<Custody> current_directory, RefPtr<Custody> executable, TTY* tty, UnveilNode unveil_tree); Process(NonnullOwnPtr<KString> name, NonnullRefPtr<Credentials>, ProcessID ppid, bool is_kernel_process, RefPtr<Custody> current_directory, RefPtr<Custody> executable, TTY* tty, UnveilNode unveil_tree, UnveilNode exec_unveil_tree);
static ErrorOr<NonnullLockRefPtr<Process>> try_create(LockRefPtr<Thread>& first_thread, NonnullOwnPtr<KString> name, UserID, GroupID, ProcessID ppid, bool is_kernel_process, RefPtr<Custody> current_directory = nullptr, RefPtr<Custody> executable = nullptr, TTY* = nullptr, Process* fork_parent = nullptr); static ErrorOr<NonnullLockRefPtr<Process>> try_create(LockRefPtr<Thread>& first_thread, NonnullOwnPtr<KString> name, UserID, GroupID, ProcessID ppid, bool is_kernel_process, RefPtr<Custody> current_directory = nullptr, RefPtr<Custody> executable = nullptr, TTY* = nullptr, Process* fork_parent = nullptr);
ErrorOr<void> attach_resources(NonnullOwnPtr<Memory::AddressSpace>&&, LockRefPtr<Thread>& first_thread, Process* fork_parent); ErrorOr<void> attach_resources(NonnullOwnPtr<Memory::AddressSpace>&&, LockRefPtr<Thread>& first_thread, Process* fork_parent);
static ProcessID allocate_pid(); static ProcessID allocate_pid();
@ -878,6 +882,7 @@ private:
LockRefPtr<Timer> m_alarm_timer; LockRefPtr<Timer> m_alarm_timer;
SpinlockProtected<UnveilData> m_unveil_data; SpinlockProtected<UnveilData> m_unveil_data;
SpinlockProtected<UnveilData> m_exec_unveil_data;
OwnPtr<PerformanceEventBuffer> m_perf_event_buffer; OwnPtr<PerformanceEventBuffer> m_perf_event_buffer;

View file

@ -560,9 +560,24 @@ ErrorOr<void> Process::do_exec(NonnullLockRefPtr<OpenFileDescription> main_progr
m_environment = move(environment); m_environment = move(environment);
TRY(m_unveil_data.with([&](auto& unveil_data) -> ErrorOr<void> { TRY(m_unveil_data.with([&](auto& unveil_data) -> ErrorOr<void> {
unveil_data.state = VeilState::None; TRY(m_exec_unveil_data.with([&](auto& exec_unveil_data) -> ErrorOr<void> {
unveil_data.paths.clear(); // Note: If we have exec unveil data being waiting to be dispatched
unveil_data.paths.set_metadata({ TRY(KString::try_create("/"sv)), UnveilAccess::None, false }); // to the current execve'd program, then we apply the unveil data and
// ensure it is locked in the new program.
if (exec_unveil_data.state == VeilState::Dropped) {
unveil_data.state = VeilState::LockedInherited;
exec_unveil_data.state = VeilState::None;
unveil_data.paths = TRY(exec_unveil_data.paths.deep_copy());
} else {
unveil_data.state = VeilState::None;
exec_unveil_data.state = VeilState::None;
unveil_data.paths.clear();
unveil_data.paths.set_metadata({ TRY(KString::try_create("/"sv)), UnveilAccess::None, false });
}
exec_unveil_data.paths.clear();
exec_unveil_data.paths.set_metadata({ TRY(KString::try_create("/"sv)), UnveilAccess::None, false });
return {};
}));
return {}; return {};
})); }));

View file

@ -42,6 +42,14 @@ ErrorOr<FlatPtr> Process::sys$fork(RegisterState& regs)
}); });
})); }));
TRY(m_exec_unveil_data.with([&](auto& parent_exec_unveil_data) -> ErrorOr<void> {
return child->m_exec_unveil_data.with([&](auto& child_exec_unveil_data) -> ErrorOr<void> {
child_exec_unveil_data.state = parent_exec_unveil_data.state;
child_exec_unveil_data.paths = TRY(parent_exec_unveil_data.paths.deep_copy());
return {};
});
}));
// Note: We take the spinlock of Process::all_instances list because we need // Note: We take the spinlock of Process::all_instances list because we need
// to ensure that when we take the jail spinlock of two processes that we don't // to ensure that when we take the jail spinlock of two processes that we don't
// run into a deadlock situation because both processes compete over each other Jail's // run into a deadlock situation because both processes compete over each other Jail's

View file

@ -7,6 +7,7 @@
#include <AK/RefPtr.h> #include <AK/RefPtr.h>
#include <AK/StringView.h> #include <AK/StringView.h>
#include <Kernel/API/Unveil.h>
#include <Kernel/FileSystem/Custody.h> #include <Kernel/FileSystem/Custody.h>
#include <Kernel/FileSystem/VirtualFileSystem.h> #include <Kernel/FileSystem/VirtualFileSystem.h>
#include <Kernel/KLexicalPath.h> #include <Kernel/KLexicalPath.h>
@ -25,6 +26,55 @@ static void update_intermediate_node_permissions(UnveilNode& root_node, UnveilAc
} }
} }
static ErrorOr<void> update_unveil_data(Process::UnveilData& locked_unveil_data, StringView unveiled_path, UnveilAccess new_permissions)
{
auto path_parts = KLexicalPath::parts(unveiled_path);
auto it = path_parts.begin();
// Note: For the sake of completence, we check if the locked state was inherited
// by an execve'd sequence. If that is the case, just silently ignore this.
if (locked_unveil_data.state == VeilState::LockedInherited)
return {};
// NOTE: We have to check again, since the veil may have been locked by another thread
// while we were parsing the arguments.
if (locked_unveil_data.state == VeilState::Locked)
return EPERM;
auto& matching_node = locked_unveil_data.paths.traverse_until_last_accessible_node(it, path_parts.end());
if (it.is_end()) {
// If the path has already been explicitly unveiled, do not allow elevating its permissions.
if (matching_node.was_explicitly_unveiled()) {
if (new_permissions & ~matching_node.permissions())
return EPERM;
}
// It is possible that nodes that are "grandchildren" of the matching node have already been unveiled.
// This means that there may be intermediate nodes between this one and the unveiled "grandchildren"
// that inherited the current node's previous permissions. Those nodes now need their permissions
// updated to match the current node.
if (matching_node.permissions() != new_permissions)
update_intermediate_node_permissions(matching_node, new_permissions);
matching_node.metadata_value().explicitly_unveiled = true;
matching_node.metadata_value().permissions = new_permissions;
locked_unveil_data.state = VeilState::Dropped;
return {};
}
auto new_unveiled_path = TRY(KString::try_create(unveiled_path));
TRY(matching_node.insert(
it,
path_parts.end(),
{ move(new_unveiled_path), new_permissions, true },
[](auto& parent, auto& it) -> ErrorOr<Optional<UnveilMetadata>> {
auto path = TRY(KString::formatted("{}/{}", parent.path(), *it));
return UnveilMetadata(move(path), parent.permissions(), false);
}));
VERIFY(locked_unveil_data.state != VeilState::Locked);
locked_unveil_data.state = VeilState::Dropped;
return {};
}
ErrorOr<FlatPtr> Process::sys$unveil(Userspace<Syscall::SC_unveil_params const*> user_params) ErrorOr<FlatPtr> Process::sys$unveil(Userspace<Syscall::SC_unveil_params const*> user_params)
{ {
VERIFY_NO_PROCESS_BIG_LOCK(this); VERIFY_NO_PROCESS_BIG_LOCK(this);
@ -35,7 +85,17 @@ ErrorOr<FlatPtr> Process::sys$unveil(Userspace<Syscall::SC_unveil_params const*>
return 0; return 0;
} }
if (veil_state() == VeilState::Locked) if (!((params.flags & to_underlying(UnveilFlags::CurrentProgram)) || (params.flags & to_underlying(UnveilFlags::AfterExec))))
return EINVAL;
// Note: If we inherited a locked state, then silently ignore the unveil request,
// and let the user program potentially deal with an ENOENT error later on.
if ((params.flags & static_cast<unsigned>(UnveilFlags::CurrentProgram)) && veil_state() == VeilState::LockedInherited)
return 0;
// Note: We only lock the unveil state for current program, while allowing adding
// indefinitely unveil data before doing the actual exec().
if ((params.flags & static_cast<unsigned>(UnveilFlags::CurrentProgram)) && veil_state() == VeilState::Locked)
return EPERM; return EPERM;
if (!params.path.characters || !params.permissions.characters) if (!params.path.characters || !params.permissions.characters)
@ -93,48 +153,24 @@ ErrorOr<FlatPtr> Process::sys$unveil(Userspace<Syscall::SC_unveil_params const*>
return custody_or_error.release_error(); return custody_or_error.release_error();
} }
auto path_parts = KLexicalPath::parts(new_unveiled_path->view()); if (params.flags & static_cast<unsigned>(UnveilFlags::CurrentProgram)) {
auto it = path_parts.begin(); TRY(unveil_data().with([&](auto& data) -> ErrorOr<void> {
return m_unveil_data.with([&](auto& unveil_data) -> ErrorOr<FlatPtr> { TRY(update_unveil_data(data, new_unveiled_path->view(), static_cast<UnveilAccess>(new_permissions)));
// NOTE: We have to check again, since the veil may have been locked by another thread return {};
// while we were parsing the arguments. }));
if (unveil_data.state == VeilState::Locked) }
return EPERM;
auto& matching_node = unveil_data.paths.traverse_until_last_accessible_node(it, path_parts.end()); if (params.flags & static_cast<unsigned>(UnveilFlags::AfterExec)) {
if (it.is_end()) { TRY(exec_unveil_data().with([&](auto& data) -> ErrorOr<void> {
// If the path has already been explicitly unveiled, do not allow elevating its permissions. // Note: The only valid way to get into this state is by using unveil before doing
if (matching_node.was_explicitly_unveiled()) { // an actual exec with the UnveilFlags::AfterExec flag. Then this state is applied on
if (new_permissions & ~matching_node.permissions()) // the actual new program unveil data, and never on the m_exec_unveil_data.
return EPERM; VERIFY(data.state != VeilState::LockedInherited);
} TRY(update_unveil_data(data, new_unveiled_path->view(), static_cast<UnveilAccess>(new_permissions)));
return {};
// It is possible that nodes that are "grandchildren" of the matching node have already been unveiled. }));
// This means that there may be intermediate nodes between this one and the unveiled "grandchildren" }
// that inherited the current node's previous permissions. Those nodes now need their permissions return 0;
// updated to match the current node.
if (matching_node.permissions() != new_permissions)
update_intermediate_node_permissions(matching_node, (UnveilAccess)new_permissions);
matching_node.metadata_value().explicitly_unveiled = true;
matching_node.metadata_value().permissions = (UnveilAccess)new_permissions;
unveil_data.state = VeilState::Dropped;
return 0;
}
TRY(matching_node.insert(
it,
path_parts.end(),
{ new_unveiled_path.release_nonnull(), (UnveilAccess)new_permissions, true },
[](auto& parent, auto& it) -> ErrorOr<Optional<UnveilMetadata>> {
auto path = TRY(KString::formatted("{}/{}", parent.path(), *it));
return UnveilMetadata(move(path), parent.permissions(), false);
}));
VERIFY(unveil_data.state != VeilState::Locked);
unveil_data.state = VeilState::Dropped;
return 0;
});
} }
} }

View file

@ -8,6 +8,7 @@
#include <AK/ScopedValueRollback.h> #include <AK/ScopedValueRollback.h>
#include <AK/String.h> #include <AK/String.h>
#include <AK/Vector.h> #include <AK/Vector.h>
#include <Kernel/API/Unveil.h>
#include <LibCore/File.h> #include <LibCore/File.h>
#include <alloca.h> #include <alloca.h>
#include <assert.h> #include <assert.h>
@ -965,6 +966,7 @@ int pledge(char const* promises, char const* execpromises)
int unveil(char const* path, char const* permissions) int unveil(char const* path, char const* permissions)
{ {
Syscall::SC_unveil_params params { Syscall::SC_unveil_params params {
static_cast<int>(UnveilFlags::CurrentProgram),
{ path, path ? strlen(path) : 0 }, { path, path ? strlen(path) : 0 },
{ permissions, permissions ? strlen(permissions) : 0 } { permissions, permissions ? strlen(permissions) : 0 }
}; };

View file

@ -25,6 +25,7 @@
#include <unistd.h> #include <unistd.h>
#ifdef AK_OS_SERENITY #ifdef AK_OS_SERENITY
# include <Kernel/API/Unveil.h>
# include <LibCore/Account.h> # include <LibCore/Account.h>
# include <LibSystem/syscall.h> # include <LibSystem/syscall.h>
# include <serenity.h> # include <serenity.h>
@ -91,6 +92,7 @@ static ErrorOr<void> unveil_dynamic_loader()
constexpr auto dynamic_loader_permissions = "x"sv; constexpr auto dynamic_loader_permissions = "x"sv;
Syscall::SC_unveil_params params { Syscall::SC_unveil_params params {
static_cast<int>(UnveilFlags::CurrentProgram),
{ dynamic_loader_path.characters_without_null_termination(), dynamic_loader_path.length() }, { dynamic_loader_path.characters_without_null_termination(), dynamic_loader_path.length() },
{ dynamic_loader_permissions.characters_without_null_termination(), dynamic_loader_permissions.length() }, { dynamic_loader_permissions.characters_without_null_termination(), dynamic_loader_permissions.length() },
}; };
@ -110,6 +112,20 @@ ErrorOr<void> unveil(StringView path, StringView permissions)
TRY(unveil_dynamic_loader()); TRY(unveil_dynamic_loader());
Syscall::SC_unveil_params params { Syscall::SC_unveil_params params {
static_cast<int>(UnveilFlags::CurrentProgram),
{ parsed_path.characters(), parsed_path.length() },
{ permissions.characters_without_null_termination(), permissions.length() },
};
int rc = syscall(SC_unveil, &params);
HANDLE_SYSCALL_RETURN_VALUE("unveil", rc, {});
}
ErrorOr<void> unveil_after_exec(StringView path, StringView permissions)
{
auto const parsed_path = TRY(Core::SessionManagement::parse_path_with_sid(path));
Syscall::SC_unveil_params params {
static_cast<int>(UnveilFlags::AfterExec),
{ parsed_path.characters(), parsed_path.length() }, { parsed_path.characters(), parsed_path.length() },
{ permissions.characters_without_null_termination(), permissions.length() }, { permissions.characters_without_null_termination(), permissions.length() },
}; };

View file

@ -36,6 +36,7 @@ namespace Core::System {
ErrorOr<void> beep(); ErrorOr<void> beep();
ErrorOr<void> pledge(StringView promises, StringView execpromises = {}); ErrorOr<void> pledge(StringView promises, StringView execpromises = {});
ErrorOr<void> unveil(StringView path, StringView permissions); ErrorOr<void> unveil(StringView path, StringView permissions);
ErrorOr<void> unveil_after_exec(StringView path, StringView permissions);
ErrorOr<void> sendfd(int sockfd, int fd); ErrorOr<void> sendfd(int sockfd, int fd);
ErrorOr<int> recvfd(int sockfd, int options); ErrorOr<int> recvfd(int sockfd, int options);
ErrorOr<void> ptrace_peekbuf(pid_t tid, void const* tracee_addr, Bytes destination_buf); ErrorOr<void> ptrace_peekbuf(pid_t tid, void const* tracee_addr, Bytes destination_buf);