-
Notifications
You must be signed in to change notification settings - Fork 1
fix(fact-ebpf): improve kernel compatibility #125
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| // clang-format off | ||
| #include "types.h" | ||
| #include "maps.h" | ||
| #include "d_path.h" | ||
|
|
||
| #include "vmlinux.h" | ||
|
|
||
|
|
@@ -21,10 +22,13 @@ __always_inline static void path_write_char(char* p, unsigned int offset, char c | |
| *path_safe_access(p, offset) = c; | ||
| } | ||
|
|
||
| __always_inline static struct bound_path_t* path_read(struct path* path) { | ||
| __always_inline static struct bound_path_t* _path_read(struct path* path, bool use_bpf_d_path) { | ||
| struct bound_path_t* bound_path = get_bound_path(); | ||
| if (bound_path == NULL) { | ||
| return NULL; | ||
| } | ||
|
|
||
| bound_path->len = bpf_d_path(path, bound_path->path, PATH_MAX); | ||
| bound_path->len = use_bpf_d_path ? bpf_d_path(path, bound_path->path, PATH_MAX) : d_path(path, bound_path->path, PATH_MAX); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL that bpf_d_path is already legacy :) But for a good reason, looks like there is a possibility of concurrent modification of the path structure, leading to use-after-free -- I assume the same is correct for using copied
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, the copied |
||
| if (bound_path->len <= 0) { | ||
| return NULL; | ||
| } | ||
|
|
@@ -35,6 +39,14 @@ __always_inline static struct bound_path_t* path_read(struct path* path) { | |
| return bound_path; | ||
| } | ||
|
|
||
| __always_inline static struct bound_path_t* path_read(struct path* path) { | ||
| return _path_read(path, true); | ||
| } | ||
|
|
||
| __always_inline static struct bound_path_t* path_read_no_d_path(struct path* path) { | ||
| return _path_read(path, false); | ||
| } | ||
|
|
||
| enum path_append_status_t { | ||
| PATH_APPEND_SUCCESS = 0, | ||
| PATH_APPEND_INVALID_LENGTH, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // clang-format off | ||
| #include "vmlinux.h" | ||
|
|
||
| #include "bound_path.h" | ||
|
|
||
| #include <bpf/bpf_helpers.h> | ||
| #include <bpf/bpf_tracing.h> | ||
| // clang-format on | ||
|
|
||
| SEC("lsm/path_unlink") | ||
| int BPF_PROG(check_path_unlink_supports_bpf_d_path, struct path* dir, struct dentry* dentry) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there is another option. From what I understand we know the exact commit (which on bpf-next resolves to v6.7-rc3), where an LSM hook on path_unlink became capable of calling bpf_d_path. How about just doing a condition on the kernel version?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered this, but RHEL 9 uses 5.14.0 for all its kernels, we would need to figure out when/if the change was backported and have a separate check for that, I figured probing was more reliable than that. |
||
| struct bound_path_t* p = path_read(dir); | ||
| bpf_printk("dir: %s", p->path); | ||
| return 0; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| #pragma once | ||
|
|
||
| // clang-format off | ||
| #include "maps.h" | ||
| #include "vmlinux.h" | ||
|
|
||
| #include <bpf/bpf_helpers.h> | ||
| #include <bpf/bpf_core_read.h> | ||
| // clang-format on | ||
|
|
||
| /** | ||
| * Reimplementation of the kernel d_path function. | ||
| * | ||
| * We should attempt to use bpf_d_path when possible, but you can't on | ||
| * values that have been read using the bpf_probe_* helpers. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any clarification on why? I assume because bpf_d_path requires a path structure with BTF information, and bpf_probe_* helpers do not provide it, is that so?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I try to use BPF_CORE_READ_INTO(&path, task, mm, exe_file, f_path);
bpf_d_path(&path, p->lineage[i].exe_path, PATH_MAX);I get the following verifier error: Searching for the error I found this answer on SO: https://stackoverflow.com/a/79348591 So it seems that you can't use variables in the stack with On newer kernels we can probably use |
||
| */ | ||
| __always_inline static long d_path(const struct path* path, char* buf, int buflen) { | ||
| if (buflen <= 0) { | ||
| return -1; | ||
| } | ||
|
|
||
| struct helper_t* helper = get_helper(); | ||
| if (helper == NULL) { | ||
| return -1; | ||
| } | ||
|
|
||
| struct task_struct* task = (struct task_struct*)bpf_get_current_task(); | ||
| int offset = (buflen - 1) & (PATH_MAX - 1); | ||
| helper->buf[offset] = '\0'; // Ensure null termination | ||
|
|
||
| struct path root; | ||
| BPF_CORE_READ_INTO(&root, task, fs, root); | ||
| struct mount* mnt = container_of(path->mnt, struct mount, mnt); | ||
| struct dentry* dentry = BPF_CORE_READ(path, dentry); | ||
|
|
||
| for (int i = 0; i < 16 && (dentry != root.dentry || &mnt->mnt != root.mnt); i++) { | ||
| struct dentry* parent = BPF_CORE_READ(dentry, d_parent); | ||
| struct dentry* mnt_root = BPF_CORE_READ(mnt, mnt.mnt_root); | ||
|
|
||
| if (dentry == mnt_root) { | ||
| struct mount* m = BPF_CORE_READ(mnt, mnt_parent); | ||
| if (m != mnt) { | ||
| dentry = BPF_CORE_READ(mnt, mnt_mountpoint); | ||
| mnt = m; | ||
| continue; | ||
| } | ||
| break; | ||
| } | ||
|
|
||
| if (dentry == parent) { | ||
| return -1; | ||
| } | ||
|
|
||
| struct qstr d_name; | ||
| BPF_CORE_READ_INTO(&d_name, dentry, d_name); | ||
| int len = d_name.len; | ||
| if (len <= 0 || len >= buflen) { | ||
| return -1; | ||
| } | ||
|
|
||
| offset -= len; | ||
| if (offset <= 0) { | ||
| return -1; | ||
| } | ||
|
|
||
| if (bpf_probe_read_kernel(&helper->buf[offset], len, d_name.name) != 0) { | ||
| return -1; | ||
| } | ||
|
|
||
| offset--; | ||
| if (offset <= 0) { | ||
| return -1; | ||
| } | ||
| helper->buf[offset] = '/'; | ||
|
|
||
| dentry = parent; | ||
| } | ||
|
|
||
| bpf_probe_read_str(buf, buflen, &helper->buf[offset]); | ||
| return buflen - offset; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,10 @@ __always_inline static struct helper_t* get_helper() { | |
| return bpf_map_lookup_elem(&helper_map, &zero); | ||
| } | ||
|
|
||
| /** | ||
| * A map with a single entry, determining whether prefix filtering | ||
| * should be done based on the `path_prefix` map. | ||
| */ | ||
| struct { | ||
| __uint(type, BPF_MAP_TYPE_ARRAY); | ||
| __type(key, __u32); | ||
|
|
@@ -38,7 +42,10 @@ struct { | |
| __always_inline static bool filter_by_prefix() { | ||
| unsigned int zero = 0; | ||
| char* res = bpf_map_lookup_elem(&filter_by_prefix_map, &zero); | ||
| return *res != 0; | ||
|
|
||
| // The NULL check is simply here to satisfy some verifiers, the result | ||
| // will never actually be NULL. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would it never be NULL? From what I see the corresponding lookup function can return NULL in some cases.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. First of, this is not the trie map, this is a regular map with a single entry that determines whether the trie map has anything and if it should be used for filtering. Because the map only has one value and it is allocated when the probe is first loaded, and because we are always asking for that one entry (by using This is further validated by the fact this code ran without the null check on newer kernels that are smarter about this kind of thing.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, and suggest now to add few more lines of commentaries about this around the |
||
| return res == NULL || *res != 0; | ||
| } | ||
|
|
||
| struct { | ||
|
|
@@ -82,6 +89,7 @@ __always_inline static struct bound_path_t* get_bound_path() { | |
|
|
||
| struct { | ||
| __uint(type, BPF_MAP_TYPE_RINGBUF); | ||
| __uint(max_entries, 8 * 1024 * 1024); | ||
| } rb SEC(".maps"); | ||
|
|
||
| struct { | ||
|
|
@@ -97,5 +105,6 @@ __always_inline static struct metrics_t* get_metrics() { | |
| } | ||
|
|
||
| uint64_t host_mount_ns; | ||
| volatile const bool path_unlink_supports_bpf_d_path; | ||
|
|
||
| // clang-format on | ||
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'm starting to have a second though on how much this is needed. We did not plan originally to have path_unlink in the first milestone, even more so backward compatible implementation for older kernels. At the same time the implementation complexity seems to be quite significant because of that. How about keep it tight, and record lack of path_unlink support for older versions as a known limitation?
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 know, but at this point, the distinction between m1 and m2 is pretty blurry + we are running out of time and we need to get things done, that includes implementing at least a basic set of file operations IMO.
The changes are needed for fact to be compatible with RHCOS 4.15 and 4.18 (RHEL 9), so it's not like I'm backporting to ancient kernels, this is very much versions being used in production right now.
I wouldn't consider loading a small probe to check whether we can use
bpf_d_pathinpath_unlinkhooks and setting a global variable to be too complex. If you are concerned about thed_pathimplementation, that was already implemented, I just moved it to its own file because I was getting circular includes and moving it out to its own header was the most straightforward solution. This implementation is also needed to get the exe_path from the process, so it's not like we can just get rid of it.Problem with this is we don't just not have
path_unlinksupport, the probe is rejected by the verifier, so we would still need to at least have the checks in place to know when we should attempt to loadpath_unlinkand when to skip it.