-
Notifications
You must be signed in to change notification settings - Fork 256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial implementation of landlock calls #903
base: master
Are you sure you want to change the base?
Conversation
Can one of the admins verify this patch? |
0c95e3d
to
394de48
Compare
Codecov ReportBase: 81.44% // Head: 84.17% // Increases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## master #903 +/- ##
==========================================
+ Coverage 81.44% 84.17% +2.72%
==========================================
Files 137 137
Lines 4920 4953 +33
==========================================
+ Hits 4007 4169 +162
+ Misses 913 784 -129
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
ok to test |
e2644de
to
7f33dfa
Compare
Landlock applies |
1bdd49c
to
7c5316f
Compare
Given that stuff is now functional, we can begin to review this. |
93f63b4
to
81d4bb3
Compare
Would this also mean we can do away with some of the header files that we copied? |
No, we need them to be able to do the initial Landlock calls to figure out if Landlock is supported at all. |
A helper header is provided so as to avoid issues with missing headers, so we can compile unconditionally.
a2c7e1e
to
e8f4ed0
Compare
One key note to make here is that WRITE permissions imply READ permissions, because otherwise, calls with O_RDWR will fail even if one rule grants read and another grants write. Another thing to note is that under landlock, only the main process can access /proc. This is because we can't add rules when the children spawn.
Note that under the current version of landlock, some syscalls are not handled by landlock, including `stat` and `access`.
Under landlock, linking and renaming throw EXDEV if `src` and `dst` are not in the same directory. This is unacceptable for us, so we simulate the calls. Because we are simulating, we want to make sure that `flags` is zero for `linkat`, otherwise it's likely that we have done something unintended.
Under landlock, since /proc is not accessible in a subprocess, SCALA calls `mincore`. We allow this in order for it to pass. Also, since `execve` is checked under landlock, we need to add `/bin` to the list of readable directories.
de66427
to
d20bd11
Compare
|
||
int landlock_version = get_landlock_version(); | ||
if (landlock_version < 2) { | ||
// ABI not old enough. Skip to seccomp |
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.
// ABI not old enough. Skip to seccomp | |
// ABI not new enough. Skip to seccomp |
LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_READ_DIR | | ||
LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_SYM | LANDLOCK_ACCESS_FS_MAKE_DIR | | ||
LANDLOCK_ACCESS_FS_REFER)) { | ||
// landlock_add_rules logs errors |
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.
Could this comment have more details? I don't understand what it's referring to, is this comment unrelated to the following close
?
return 0; | ||
return -1; | ||
#else | ||
return 0; // FreeBSD does not have landlock |
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.
We should abort()
here.
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.
How? Consumers of this function throw a python error, but what would be appropriate to do here?
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.
Got it. I think has_landlock
should be implemented here rather than in Python, so we can return false order FREEBSD.
#define LANDLOCK_ACCESS_FS_MAKE_SYM (1ULL << 12) | ||
#endif /* _LINUX_LANDLOCK_H */ | ||
|
||
// Not always defined, depends on ABI version. |
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.
ifndef
?
@@ -0,0 +1,3 @@ | |||
#include "landlock_header.h" | |||
|
|||
int landlock_add_rules(const int ruleset_fd, const char **paths, __u64 access_rule); |
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.
Could we name this cptbox_landlock_add_rules
? Makes it easier to keep track of what's provided by the kernel versus by us.
|
||
if (rule.parent_fd < 0) { | ||
if (errno == ENOENT) | ||
goto close_fd; // missing files are ignored |
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.
Nit: parens even around single-statement branches / loops / etc. I know not all cptbox code is written like this, but all new code should be.
if security is not None: | ||
self.configure_files(security.read_fs, security.write_fs) | ||
else: | ||
self.configure_files([], []) |
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.
Is this branch necessary at all? Seems a little weird to have.
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.
Unsure. security
defaults to None. I don't know why.
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.
My question was more: it seems weird that the default uninitialized state would be different from initializing with [].
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.
We could maybe avoid it by having a default of empty list in _cptbox.pyx
, but I don't see how else to avoid it. configure_files
is the function that sets the landlock_*
attributes.
We could, in theory, have them defaulted to empty and then have this branch to set them to something, but I don't love that.
@@ -223,6 +244,8 @@ def wait(self) -> int: | |||
raise RuntimeError('failed to spawn child') | |||
elif self.returncode == PTBOX_SPAWN_FAIL_SETAFFINITY: | |||
raise RuntimeError('failed to set child affinity') | |||
elif self.returncode == PTBOX_SPAWN_FAIL_LANDLOCK: | |||
raise RuntimeError('landlock configuration failed') |
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.
raise RuntimeError('landlock configuration failed') | |
raise RuntimeError('Landlock configuration failed') |
Nit, here and in other user-facing strings as well as comments. The kernel docs refer to it as "Landlock" (despite "seccomp" being lowercased).
@@ -20,6 +20,7 @@ class Executor(JavaExecutor): | |||
ExactFile('/bin/bash'), | |||
RecursiveDir('/etc/alternatives'), | |||
] | |||
compiler_syscalls = ['mincore'] |
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.
Why did this change?
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.
SCALA takes a different path under landlock last I checked, and that path requires mincore
. I believe it had to do with how landlock deals with /proc/<pid>
.
We decided at some point to simply allow the call.
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.
Ack, thanks for explaining. Could we split this out in its own commit?
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.
Eh, I guess it kind of is already. Fine to leave it as-is.
int get_landlock_version() { | ||
#if !PTBOX_FREEBSD | ||
char *sandbox_mode = getenv("DMOJ_SANDBOX_MODE"); | ||
if (sandbox_mode != nullptr && strcmp(sandbox_mode, "ptrace+seccomp") == 0) { |
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.
This is a surprising place for this check to live, and I think the check should be stricter than this.
We should validate that the string is one of:
ptrace+seccomp
ptrace+seccomp+landlock
auto
and otherwise bail out. Otherwise we can't confidently make changes to the interpretation of this environment variable, as we'd have been too lax in its inputs in earlier versions.
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.
I agree that this location is strange. Where should it live?
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.
I think living in this file is probably fine, and it should return an enum. Then it should be consulted in the spawn routine alongside the Landlock version we have available. My concern here is more about subtly overloading the semantics of get_landlock_version
than anything else.
We should bail if someone requests:
- seccomp or seccomp+landlock support on FreeBSD
- ptrace-only support on Linux
- seccomp+landlock on a Linux with too old a Landlock ABI
This sanity check could exist in judge.py
alongside all the other sanity checks, but I don't feel strongly about that.
@@ -27,8 +28,16 @@ struct child_config { | |||
int *seccomp_handlers; | |||
// 64 cores ought to be enough for anyone. | |||
unsigned long cpu_affinity_mask; | |||
const char **landlock_read_exact_files; |
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.
This may end up looking a little cleaner if we have a
struct {
const char **read_exact_files;
...
} landlock;
or even
const struct {
char **read_exact_files;
...
} landlock;
assert self._executable is not None | ||
# Under landlock we need this for execve to work. | ||
# We use `self._executable` because it is copied when caching executors, but other properties are not. | ||
return super().get_fs() + [ExactFile(self._executable)] |
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.
The fact that both get_executable
and get_compiled_file
exist but return possibly different values is an abomination, and we should fix this. You don't have to do so in this PR, but please leave a comment to the effect of this being a hack to work around that.
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.
The problem isn't because get_executable
and get_compiled_file
both exist. Indeed, I expect them to return the same file for RUST.
The reason they don't is because the cached executor isn't a full copy of the original. Instead, it simply sets _executable
and _dir
on the returned object, and nothing else.
For rust, that means that we try and lock a new directory, which is obviously wrong.
To bypass it I use _executable
here, but the proper fix may be instead to look into the caching.
Here's the relevant line: https://github.com/DMOJ/judge-server/blob/master/dmoj/executors/compiled_executor.py#L58
Also, as for get_executable
vs get_compiled_file
, check out Java's usage of the methods:
https://github.com/DMOJ/judge-server/blob/master/dmoj/executors/java_executor.py#L82
Maybe these exist because Java's compiled file isn't the executable?
@@ -33,6 +33,7 @@ | |||
ExactFile('/dev/urandom'), | |||
ExactFile('/dev/random'), | |||
*USR_DIR, | |||
RecursiveDir('/bin'), # required under landlock when /bin is not a symlink, since we check execve. |
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.
Fine to not leave a comment here.
return PTBOX_SPAWN_FAIL_LANDLOCK; | ||
} | ||
|
||
// Note: WRITE must imply READ. This is required because even if one rule allows writing and another allows reading, |
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.
The O_TRUNC
handling in Landlock scares me. Could we add some testcases with a custom grader that make sure that we don't allow truncating etc.?
No description provided.