So I tried to implement whatever I was talking about last time. (Or writing, whatever.) A program that uses ptrace(2) to track it's children and puts each into it's own network namespace.
Here's Github link And this is the current commit: 48f02327dda7db6db7c1133ce96618f4902e7d4d.
So, where do I start?
So, as I mentioned last time, I wanted to use Go. I tried multiple ptrace libraries, and not a single one worked. I'm pretty sure that is because the 'exec.Command' does not work properly. And by properly, I mean not in a way that works with ptrace.
It took me way too long to realise this and move on...
I don't hate Zig. I actually like quite a few things about Zig. That is why I decided to try it next. It's been quite a while since I used it last time, so why not try it now? Well, quite a few reasons really.
First of all, not all the usual syscalls/wrappers are accessible, so that's a thing. But that's not all. While some are modified to take Zig types, others require C-compatible types. And let me tell you, sometimes convincing Zig to transform whatever you have to the desired type is... not fun. That's not only a C thing, sometimes getting the correct Zig type can be complicated.
I'm sure there are some very good reasons for all this, but I sill kinda prefer the simplicity of C at times...
Speaking of which:
C might not be the attractive language people are into these days. Sure, it can feel a bit antiquated at times. It misses a lot of useful features that are common in modern languages. It's often not the first language you reach for, but it doesn't mind.
It's always there for you, waiting, until something fails and you fall back to it. Because when you do, you can be sure that it WILL work and there WILL be at least some library to help on your way.
Well, at least as long as you're not trying to do the impossible...
So, do you remember how I was limited by the way Linux handles filesystems while working in the RFS? Well, we're back!
You see, I was, so far, unable to figure out how to move a running process to a different namespace. I will have to figure something out. Either how to overcome this, or some other way to give each process a web server. (Maybe I should look into *nix sockets?)
Well, I guess I'll just share some C and call it a day for now?
So, how do you ptrace? Well, first you have to have a process. To do that, you have to fork(2).
Forking clones a process and returns '0' to the new process and the new PID to the original process. The new process is considered a child of the original, so it can be ptraced. I just execvp(3) it, replacing it with a new process, keeping it's PID and place within the system hierarchy.
Ok, so now you have a child, but what do you do with the parent? Well, you ptrace from it of course. Something like this:
if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) != -1) { while (!quit) { if (waitpid(pid, &status, __WALL) < 0) { perror("waitpid failed"); break; } if (WIFEXITED(status)) { printf("Child exited with status %d\n", WEXITSTATUS(status)); break; } if (ptrace(PTRACE_GETREGS, pid, NULL, ®s) == -1) { perror("ptrace PTRACE_GETREGS failed"); break; } handleSyscall(pid, ®s, &entering); if (ptrace(PTRACE_SYSCALL, pid, 0, 0) == -1) { perror("ptrace PTRACE_SYSCALL failed"); break; } entering = !entering; } }
You wait for a syscall, catch it, handle it, resume it and repeat. Oh yea, you also handle it.
// TODO: ARM support #ifdef __x86_64__ #define SYSCALL_CODE(regs) ((regs)->orig_rax) #define ARG1(regs) ((regs)->rdi ) #define ARG2(regs) ((regs)->rsi ) #define ARG3(regs) ((regs)->rdx ) #define ARG4(regs) ((regs)->r10 ) #define ARG5(regs) ((regs)->r8 ) #define ARG6(regs) ((regs)->r9 ) #define RETURN_VALUE(regs) ((regs)->rax ) #define CODE_INVALID -38 #define CODE_EXECVE 59 #define CODE_FORK 57 #define CODE_VFORK 58 #define CODE_CLONE 56 #define CODE_CLONE3 435 #endif // TODO: handle CLONE_THREAD // Will attach new handleProcess to new processes created by fork, vfork, clone, // or clone3 syscalls. void handleSyscall(pid_t pid, struct user_regs_struct* regs, int* entering) { if (!*entering && (SYSCALL_CODE(regs) == CODE_FORK || SYSCALL_CODE(regs) == CODE_VFORK || SYSCALL_CODE(regs) == CODE_CLONE || SYSCALL_CODE(regs) == CODE_CLONE3)) { pid_t* child_pid = malloc(sizeof(pid_t)); *child_pid = RETURN_VALUE(regs); // invalid syscall, just grab the next one if (*child_pid == CODE_INVALID) { *entering = !*entering; return; } printf("Intercepted syscall %lld, child pid: %d\n", SYSCALL_CODE(regs), *child_pid); pthread_t thread_id; pthread_create(&thread_id, NULL, handleProcess, (void*)child_pid); pthread_detach(thread_id); } }
Just like most things in C, syscall is just a number. Syscalls get triggered two times, both at request and response. They pass arguments in registers, so they are not portable across platforms.
You can read and write the registers, but you still have no access to the tracees memory. Well, kinda. You can request data piece by piece, but reading strings is still a bit painful.
Yes, I also tried to make each processes tracer it's each pthread(7). Not as fun as Go's goroutines, but it works. Well, not like I got go play all that much with it, give This pretty much where I am now.
Originally I wanted to catch process before it forks (or vforks/clones/clone3s) and put it into it's new habitat before it starts doing things, but that got cut short by me being unable to move processes between network namespaces...
##...
So yea, this is where I'm now and I'm not sure where I'll go. I also feel the need to play with BSD again, so We'll see.