Tuesday, October 21, 2008

linux: change the process' name

From time to time developers want to modify the command line of program that is shown in ps or top utilities.
There is no API in linux to modify the command line.
This article is linux specific but I hope that following these steps you can do it in almost every OS(BTW, FreeBSD has setproctitle routine that should do the job).

Both ps and top tools comes from procps package in all linux distribution I worked with. So they have the same base and let's stop on ps because it's a bit easier to investigate it.
To figure out how ps gets information about the processes let's simply trace it

$strace ps aux 2>&1 1>/dev/null|grep $$
stat64("/proc/4391", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
open("/proc/4391/stat", O_RDONLY)       = 6
read(6, "4391 (bash) S 4388 4391 4349 348"..., 1023) = 222
open("/proc/4391/status", O_RDONLY)     = 6
open("/proc/4391/cmdline", O_RDONLY)    = 6
readlink("/proc/4391/fd/2", "/dev/pts/1", 127) = 10
read(6, "4760 (strace) S 4391 4760 4349 3"..., 1023) = 208
read(6, "4761 (grep) S 4391 4760 4349 348"..., 1023) = 196
read(6, "grep\0004391\0", 2047)         = 10
As expected it reads information from procfs.
$cat /proc/$$/cmdline
bash
Now prepare yourself, I'm going to dig into the kernel sourses.

Looking into fs/proc/base.c I found desired function proc_pid_cmdline that is used to show the command line in /proc/<pid>/cmdline
The part of it that we are interested:
static int proc_pid_cmdline(struct task_struct *task, char * buffer) {
...
    struct mm_struct *mm = get_task_mm(task);

...

    len = mm->arg_end - mm->arg_start;
 
    if (len > PAGE_SIZE)
        len = PAGE_SIZE;

    res = access_process_vm(task, mm->arg_start, buffer, len, 0);

    // If the nul at the end of args has been overwritten, then
    // assume application is using setproctitle(3).
    if (res > 0 && buffer[res-1] != '\0' && len < PAGE_SIZE) {
        len = strnlen(buffer, res);
        if (len < res) {
            res = len;
        } else {
            len = mm->env_end - mm->env_start;
            if (len > PAGE_SIZE - res)
                len = PAGE_SIZE - res;
            res += access_process_vm(task, mm->env_start, buffer+res, len, 0);
            res = strnlen(buffer, res);
        }    
    }    
...
That's funny but in comments mentioned setproctitle(3). After I saw these lines I tried to find setproctitle for linux but failed. It's interesting why is mentioned here as it available in FreeBSD but not in linux.
Anyway let's move forward.
The most interesting parts here are
len = mm->arg_end - mm->arg_start;
 
    if (len > PAGE_SIZE)
        len = PAGE_SIZE;

    res = access_process_vm(task, mm->arg_start, buffer, len, 0);
access_process_vm, defined in mm/memory.c, accesses another process' address space. The prototype:
int access_process_vm(struct task_struct *tsk, unsigned long addr, void *buf, int len, int write)
Fifth argument write is a flag, if it is 0 then access_process_vm reads len bytes of the process' memory to buf starting from address addr.
Going back to proc_pid_cmdline we can see that start address of the string with the command line is mm->arg_start and its length is mm->arg_end - mm->arg_start but not bigger than PAGE_SIZE. PAGE_SIZE is set to 4096 bytes as defined in include/asm-i386/page.h
#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)
Well, now we know that we have 4KB where we can write.

Who fills the memmory between mm->arg_start and mm->arg_end and what is stored there?
I hope everybody uses ELF binary format now. So let's go to fs/binfmt_elf.c
The name of the function that creates process environment is create_elf_tables.
The part of it we are actually interested in:
static int
create_elf_tables(struct linux_binprm *bprm, struct elfhdr *exec,
        unsigned long load_addr, unsigned long interp_load_addr) {
...
    /* Populate argv and envp */
    p = current->mm->arg_end = current->mm->arg_start;
    while (argc-- > 0) {
        size_t len;
        if (__put_user((elf_addr_t)p, argv++))
            return -EFAULT;
        len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
        if (!len || len > MAX_ARG_STRLEN)
            return -EINVAL;
        p += len;
    }
    if (__put_user(0, argv))
        return -EFAULT;
    current->mm->arg_end = current->mm->env_start = p;
    while (envc-- > 0) {
        size_t len;
        if (__put_user((elf_addr_t)p, envp++))
            return -EFAULT;
        len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
        if (!len || len > MAX_ARG_STRLEN)
            return -EINVAL;
        p += len;
    }
    if (__put_user(0, envp))
        return -EFAULT;
    current->mm->env_end = p;
...
According to create_elf_tables argv points to current->mm->arg_start and current->mm->arg_end points to the end of the environment(envp).

To modify cmdline of the process you have to overwrite memory between current->mm->arg_start and current->mm->arg_end and to keep program's integrity move its environment.

Looking into the sources of getenv/setenv we can see that they access **environ variable(environ(7)). environ is declared in the <unistd.h> but it's preffered to declare it in the user program as
extern char **environ;
The following code rewrites cmdline of the process and moves environment to it's new 'home'.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <sys/user.h>

extern char **environ;

int main(int argc, char **argv)
{   
    unsigned int pid = getpid();
    char proc_pid_cmdline_path[PATH_MAX];
    char cmdline[PAGE_SIZE];
    
    sprintf(proc_pid_cmdline_path, "/proc/%d/cmdline", pid);
    
    FILE *proc_pid_cmdline =  fopen(proc_pid_cmdline_path, "r");
    fgets(cmdline, PAGE_SIZE, proc_pid_cmdline);
    fclose(proc_pid_cmdline);
    
    printf("%s : %s\nenvironment variable HOME = %s\n", proc_pid_cmdline_path, cmdline, getenv("HOME"));
    
    int env_len = -1;
    if (environ) 
        while (environ[++env_len])
            ;
    
    unsigned int size;
    if (env_len > 0)
        size = environ[env_len-1] + strlen(environ[env_len-1]) - argv[0];
    else
        size = argv[argc-1] + strlen(argv[argc-1]) - argv[0];
    
    if (environ)
    {
        
        char **new_environ = malloc(env_len*sizeof(char *));
        
        unsigned int i = -1;
        while (environ[++i])
            new_environ[i] = strdup(environ[i]);
        
        environ = new_environ;
    }   
        
    
    char *args = argv[0];
    memset(args, '\0', size);
    snprintf(args, size - 1, "This is a new title and it should be definetely longer than initial one. Actually we can write %d bytes long string to the title. Well, it works!", size);
    
    proc_pid_cmdline =  fopen(proc_pid_cmdline_path, "r");
    fgets(cmdline, PAGE_SIZE, proc_pid_cmdline);
    fclose(proc_pid_cmdline);
    
    printf("%s : %s\nenvironment variable HOME = %s\n", proc_pid_cmdline_path, cmdline, getenv("HOME"));
    
    return 0; 
}
The output should be
$./chcmdline 
/proc/5865/cmdline : ./chcmdline
environment variable HOME = /home/niam
/proc/5865/cmdline : This is a new title and it should be definetely longer than initial one. Actually we can write 1843 bytes long string to the title. Well, it works!
environment variable HOME = /home/niam
There is an option to expand memory region that we can overwrite. Just assign environment variable for the process:
$ENV_VAR=$(perl -e 'print "0" x 4096;') ./chcmdline 
/proc/5863/cmdline : ./chcmdline
environment variable HOME = /home/niam
/proc/5863/cmdline : This is a new title and it should be definetely longer than initial one. Actually we can write 5948 bytes long string to the title. Well, it works!
environment variable HOME = /home/niam
That small perl script prints "0" 4096 times to stdout, so as we can see the memory size of environment grew up to 5948 bytes.

This code might be portable to other OSes, I don't know. In general in POSIX system with gcc compiler you might be successfull with this code.

15 comments:

Cheba said...

Superb!

Is there any chance this can be achieved in scripting environments?

Ni@m said...

If you can get an access to original argv and environ pointers you can do that. I suppose this can be done in c extensions to the scripting language if it pass original pointers to argv and environ.

Anyway, with some arch/os/etc.-dependent hacks you can find the addresses of argv and environ.
You can find some information that might help to find the addresses in /proc/<pid>/{maps,environ}.

Also you can write a kernel syscall to change the world ;).
That's a good point for me BTW 8)

Cheba said...

Is there any chance I can find anytime soon a kernel patch for setproctitle on Linux? ;)

Ni@m said...

I think you shouldn't expect such patch in the nearest 10 years.
The request of setproctitle was raised several times in mail lists but did not have success.

BTW, I've found implementation of setproctitle in util-linux-ng sources. Looks like everybody in linux kernel is satisfied with such implementation of setproctitle so nothing is going to be changed.

coolvibe said...

Just overwrite argv[0] and be done with it. Works for almost all unixen. If you want to do it neatly, just write your own setproctitle in userland like so:

https://svn.habolinux.de/filedetails.php?repname=util-linux&path=%2Flib%2Fsetproctitle.c&rev=1&sc=1

No kernel hacks necessary.

Ni@m said...

I didn't perform any kernel hack. I just showed how some things are being done in linux kernel to ensure what we can do in the userland.
So code that actually changes the 'process name' just overwrites data reserved for argv and env.

garion said...

Just an FYI- I just implemented a version of this, based on your code. I found a bug, where you allocate the new_env.. It should be: malloc(env_len*sizeof(char*));

Otherwise, you dont allocate enough.

Ni@m said...

garison, you are completely right.
Thank you for pointing on that. I've updated the code snippet.

Milo said...

unsigned int env_len = -1;
if (environ)
while (environ[++env_len])
;

unsigned int size;
if (env_len > 0)
size = environ[env_len-1] + strlen(environ[env_len-1]) - argv[0];
else
size = argv[argc-1] + strlen(argv[argc-1]) - argv[0];


there is an integer overflow in the code above, if environ is NULL (but can this happen?). env_len would then be (unsigned int)-1 = 0xffffffff, so always > 0.

Ni@m said...

Hey, Milo!
You are right!

Also if (env_len > 0) is totally nonsense here. Thanks for pointing this out. Fixing the code snippet.

nbryskin said...

Here is partly portable implementation of setproctitle in PostreSQL: http://doxygen.postgresql.org/ps__status_8c-source.html

Also, there is python binding: http://code.google.com/p/py-setproctitle/

Chris said...

This still displays original command name in top. "ps ax" displays name within the code but top will still show "command" as whatever the executable was. Is there a way to address the top part of this?

Ni@m said...

nbryskin, thanks for the link. Portable solution is always better!

However I hope that sometime this 'useless' feature will be reimplemented in a different manner: a system call in linux for change process title and updated ps that will show process title instead of command line if former exists.

Ni@m said...

Chris, I don't know what _exactly_ top shows, but likely it's path to the executable. I suppose that htop will show the same as ps.

Gearoid Murphy said...

You only allocate enough space in new_environ to handle all the current environment variables but the environ structure expects to have a trailing NULL entry:

char **new_environ = malloc((env_len+1)*sizeof(char *));
environ [env_len] = NULL;

setenv and putenv depend on this trailing NULL entry and crash otherwise.