RSoC: Implementing ptrace for Redox OS - part 4

By jD91mZM2 on

Table of Contents:

Merged (again)!

Once again, last weeks action was merged, which means the full ptrace feature was merged, and it’s time to start tackling the final issues which I have delayed for so long. But, before that, I decided to try to get some basic ptrace compatibility in relibc, so we could see just how far away software like gdb is from being ported, and what concerns I haven’t thought about yet.

redox-nix update

That said, I took a little break from the madness, to instead lay my focus on another interesting problem: Newer redoxer couldn’t be compiled using carnix, because of some dependency that used a cargo feature carnix didn’t support. Let me first explain what carnix is, and why this is a problem:

The Nix package manager, in all it’s glory, is all about reproducibility. This means one should not have any compilation cache that may make compilation work for one person but not for another. This aspect is not dissimilar from the Redox package manager, as it first downloads the source in source/, and then copies it to build/ before each build. (I’ll not dive into details in whether Redox will eventually get a more interesting package manager, but the answer is yes, hopefully!)

Consider this: You are building a crate that requires the openssl crate. You install the openssl C dependency and forget about it. Later you decide to build a package for this crate. If you have compilation cache, you might not have to rebuild the openssl crate, even if you use sandboxing or otherwise uninstall the openssl C dependency just to see whether it’s required or not. Therefore you may forget to add that as a C dependency, and break reproducibility.

Of course you don’t actually want to rebuild everything on each tiny change or update, though… That’s why carnix throws cargo in the trash can and rolls it’s own solution: Create a package for each dependency crate, and manually compile them with rustc. Throwing cargo in the trash can, along with not being great for the environment (one should really recycle things), also throws all the cool cargo-specific there as well and leaves carnix to constantly re-implement each feature, which of course breaks a lot of things.

This problem is why I started “yacna”, Yet Another Cargo->Nix Alternative. It aims to use cargo as a library to generate all the needed commands using the completely standard way, and then modify the commands only as little as possible to make things work. It’s not pretty, but locally I have managed to compile a simple crate with like one dependency. And it supports feature flags and everything out of the box, thanks to re-using Cargo. This is what I’ve spent much of the week on, and thus I won’t have too much to give that is Redox-related.

Relibc: ptrace compatibility

The relibc ptrace implementation will always be a best-effort one, as due to the nature of Redox’s vastly different (read: superior) ptrace API (which is more similar to the /proc filesystem almost every experienced ptrace user keeps talking about), it’ll always be a hellish compatibility layer which keeps track of most things in userspace. Note that Redox will, at least as long as I get anything to say about it, never implement useless or otherwise ptrace “features” for compatibility with Linux in the kernel. That’s why it’ll be very difficult to get this to work in relibc.

The first things I did was to implement a ptrace wrapper for Linux, which was of course easy when the function and the syscall map 1:1.

Next, I had some fun and implemented the Once<T> synchronization primitive. It’s interesting to implement something which is so important you get right, as locks could be attempted simultaneously and the whole goal is to handle that gracefully. I spent some time to unify the Mutex and the Once code, as they are surprisingly similar in a way.



This had to be worked around by adding a specific ptrace handler, as shown here:

// First, allow ptrace to handle waitpid
let state = ptrace::init_state();
let sessions = state.sessions.lock();
        if res == Some(0) {
            // WNOHANG, just pretend ptrace SIGSTOP:ped this
            res = Some(pid as usize);

ptrace should also add WUNTRACED when in a session, but that is a problem because of the below issue.

Issue #2: Tracemes are not implemented

Tracemes are, in my opinion, useless. The parent could just attach to the child using its process ID. It’s just problematic: PTRACE_TRACEME can be called from a child process so that the parent, without help from the kernel or the process itself, can’t really know it’s in a session. And the memory is not shared in forks. So somehow, here it needs to detect that, perhaps using IPC. Of course though, it can’t issue a blocking read and/or write in a nonblocking ptrace call, and ugh. Another alternative is using shared memory, but that means you need to tell one single BTreeSet to use a different allocator, that also somehow dynamically allocates things in a fixed view of memory.

For now, PTRACE_TRACEME is a no-op, and most ptrace operations automatically attach to the child process it’s trying to operate on. Works great, except now waitpid can’t add WUNTRACED automagically before any ptrace operation is issued on the parent. So having it a no-op is sadly not a solution. I am almost tempted to implement the ptrace compatibility function as a specialized scheme daemon which by taking care of all ptrace-related calls can share memory and therefore implement this stuff easier. But while PTRACE_TRACEME is the only issue, I think I’ll try sticking to a less overkill solution.

Image of ptrace running on Redox