cj

My name is Chris Hanson, and I’m a Software Engineer currently consulting in Madison, WI. I’m one of those oober-geeks who programs for fun instead of fame and fortune. Often I find myself searching the interwebs for solutions and hints to get me pointed in the correct direction, and occasionally I end up on someone’s blog, finding just the bits I need. I think it’s high time to contribute back to the community in the same fashion. I’ve decided to start recording my solutions, tips and tricks, and other findings. I really don’t expect subscribers or anything, just to have a place that Google can index and point you to when you’re trying solve that problem and want to see if anyone else has been in your shoes.

I have a sweet deal on a Virtual Private Server. I opted for the “Dev” model, which comes with little support and expects that you are very familiar with your chosen OS. I chose Ubuntu, which comes in 9.04. The host provides this through Virtuozzo, which is a specialized kernel that provides separation of each OS. If you can configure Linux, this is a great VPS solution. However every time I issued an apt-get, I ran into problems. The first problem was messages from any “apt-get install” which told me that there were permission denied problems:

Can't exec "[blah]": Permission denied at /usr/share/perl/[version]/IPC/Open3.pm

What’s happening is the VPS host mounts /tmp with noexec,nosuid. There is nothing wrong with that, I prefer it that way, but apt-get was not configured to deal with that. The solution is to add two lines to the end of /etc/apt/apt.conf.d/70debconf:

DPkg::Pre-Invoke{"mount -o remount,exec /tmp";};
DPkg::Post-Invoke {"mount -o remount /tmp";};

The second problem I ran into was the fact that every time I issued apt-get upgrade and then rebooted, I could no longer connect to my host. I would have to reinstall from the template using the control panel provided by my host. Ugh. Trying to troubleshoot this becomes arduous at best, because I have no alternate way to connect once the server becomes unresponsive. I can see that all the processes are running from the web-based control panel, including sshd, but I just can’t connect. My solution? I wanted to see if there was something about the upgrade that would break, so I copied my entire OS to a folder within the root folder:

mkdir /root/os
cp -a bin boot etc home lib media mnt opt sbin selinux srv tmp usr var /root/os
mkdir /root/os/root
cp -rp /root/.* /root/os/root
mkdir /root/os/dev
mkdir /root/os/proc
mkdir /root/os/sys

I chose to put things in the root folder just because I don’t like to sully the root directory. I also took care to copy in my dot files so that my bash prompt would still be pretty. Also you should not copy the system folder: dev, proc, sys. Those need to mounted. I issued the following commands to move into the copied OS. I later turned that into a script while experimenting with other OS’s:

#!/bin/sh
#
# switchto OSFOLDER

if [ -e "/root/$1" ]; then
        mount -t proc none /root/$1/proc
        mount -o bind /dev /root/$1/dev
        mount -o bind /sys /root/$1/sys
        mount -t tmpfs none /root/$1/tmp

        chroot /root/$1 /bin/bash -l

        umount /root/$1/tmp
        umount /root/$1/sys
        umount /root/$1/dev
        umount /root/$1/proc
else
        echo Negative.  "$1" is not an option.
fi

This takes care of setting up the system folders and changing the root. I type /root/switchto os and am seated comfortably in a copy of the original OS. I issue apt-get update followed by apt-get upgrade. After occupying myself for a little while I find that all my packages have been upgraded. Everything works. Just to be sure, given the previous problems I ran into, I reboot. Everything is fine. I shell in and issue another /root/switchto os, and here I am, same OS, upgraded packages. Wonderful. I run a distribution upgrade:

apt-get install update-manager-core
do-release-upgrade

After running the distribution upgrade I do the reboot test again, and I still find that I my host OS functions normally, and from a shell I can switch to the new distribution. Wonderful. I ran another distribution upgrade. Ubuntu 10.04 is the current stable distribution upgrade. You can force it upgrade to 10.10, but that is not officially supported through the upgrade path.

So last problem, I’m not actually running 10.04, I am switching to it in a single shell. I was considering a call the my switchos script from within the init.d process, but my work with my router tells me that’s a bad idea. After a bit of experimenting I decided to replace my host operating system’s init since it’s what the kernel does after it is ready to rock. I was hoping I could replace /sbin/init with a script, but no such luck. I ended up writing a C++ program to do the job.

// switchinit.c
//
// Written on: 2010.12.08
// Chris Hanson (c) 2010
//
// Copy all you want, change all you want, leave/append this header, give credit.
//
// Compile as "init"
// Rename old init as "init.original"
// This replaces that init
// Create a file named /root/.initos
// Place one string on one line indicating the path of the new root.
//
// There's lots to fix.  Like checking for trailing spaces in ".initos"
// Better usage of memory, maybe proper placement of the switched OS

#include <time.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>

char* ReadOsLocation( char* name );
int IsValidLocation( char* location );
void DumpEnv( char* name, char** envp, char** argv, int argc, char* os );

int main( int argc, char* argv[], char* envp[] )
{
        // Locals
        char szProc[1024];
        char  szSys[1024];
        char  szDev[1024];
        char szRoot[1024];
        int stream;

        // My host likes to mount tmps for me, undo that
        umount( "/tmp" );
        umount( "/var/tmp" );

        // Run injection
        if( IsValidLocation( "/sbin/injection"  ) )
        {
                // Launch primary injection (can be script)
                system( "/sbin/injection" );
        }

        // Read location
        char* location = ReadOsLocation( "/root/.initos" );

        // Ensure trailing slash
        if( location[strlen( location ) - 1] != '/' )
                location = strcat( location, "/" );

        // Do we have a valid, new location?
        if( strcmp( location, "/" ) != 0 && IsValidLocation( location ) != 0 )
        {
                // Load up string
                strcpy( szProc, location );
                strcpy( szSys, location );
                strcpy( szDev, location );
                strcpy( szRoot, location );
                strcat( szProc, "proc" );
                strcat( szSys, "sys" );
                strcat( szDev, "dev" );
                strcat( szRoot, "host" );

                // Mount system folders into destination
                mount( "none", szProc, "proc",  0,       "" );
                mount( "/sys",  szSys, "sysfs", MS_BIND, "" );
                mount( "/dev",  szDev, "",      MS_BIND, "" );
                mount(    "/", szRoot, "",      MS_BIND, "" );

                // Change root
                setsid();
                chdir( location );
                chroot( "." );
        }

        // Otherwise change target file to avoid looping
        else argv[0] = "init.original";

        // Mount our tmp
        mount( "none", "/tmp", "tmpfs", MS_NOEXEC | MS_NOSUID, "" );

        // Log
        DumpEnv( "/var/log/initos.log", envp, argv, argc, location );

        // Redirect streams for debugging
        if( ( stream = open( "/var/log/initos.log", O_RDWR | O_APPEND ) ) != -1 )
        {
                dup2( stream, STDOUT_FILENO );
                dup2( stream, STDERR_FILENO );
                close( stream );
        }

        // Run injection
        if( IsValidLocation( "/root/injection"  ) )
        {
                // Launch secondary injection (can be script)
                system( "/root/injection" );
        }

        // Move to sbin
        chdir( "/sbin" );

        // Reload using new init
        execve( argv[0], argv, envp );
}

// Reads a string from a file.  Calls malloc no matter what.
char* ReadOsLocation( char* name )
{
        // Locals
        FILE* file;
        char* buffer;
        unsigned long length;
        int c;

        // Open file
        file = fopen( name, "rb" );

        // File doesn't exist?
        if( !file )
        {
                // We're going to just use this OS
                return "/";
        }

        // Get file length
        fseek( file, 0, SEEK_END );
        length = ftell( file );
        fseek( file, 0, SEEK_SET );

        // Allocate memory
        buffer = (char *)malloc( length + 1 );

        // Memory problem?
        if( !buffer )
        {
                // Clean up
                fclose( file );

                // Run away!
                return "/";
        }

        // Read the file contents
        fread( buffer, length, 1, file );

        // Clean up
        fclose(file);

        // Trim string
        for( c = 0; c < strlen( buffer ); c++ )
        {
                if ( buffer[c] == '\n' || buffer[c] == '\r' )
                        buffer[c] = '\0';
        }

    // Return the buffer
    return buffer;
}

int IsValidLocation( char* location )
{
        // Locals
        struct stat stFileInfo;
        int intStat;

        // Zero means it's good to go
        return stat( location, &stFileInfo ) == 0 ? 1 : 0;
}

void DumpEnv( char* name, char** envp, char** argv, int argc, char* os )
{
        // Locals
        time_t curtime;
        FILE* file;
        char  **env;
        int c;

        // Open file
        file = fopen( name, "ab" );

        // File open failed!?  Bail.
        if( !file )
                return;

        // Write date/time to file
        curtime=time(NULL);
        fprintf( file, "\n\ninit at: %s", asctime(localtime(&curtime)) );
        fprintf( file, "os: %ssbin/", os );

        // Dump arguments
        for( c = 0; c < argc; c++ )
                fprintf( file, "%s ", argv[c] );

        // Separate
        fprintf( file, "\n" );

        // Dump env to file
        for( env = envp; env && *env; env++)
                fprintf( file, "%s\n", *env );

        // Separate
        fprintf( file, "\n\n" );

        // Clean up
        fclose( file );
}

This is actually the most recent version; I just couldn’t leave well enough alone. This version provides two facilities for interjecting into the “boot” process. The main facets of this program are:

Removes the hosted /tmp and /var/tmp mounts.
Runs the injection located at: /sbin/injection
Checks /root/.initos to find out what OS (folder) is the target.
Ensures the loaded target location is valid.
When the location is not valid, it switches the target "init" to "init.original"
Mounts the system folders, including the host's root into the new OS (make sure you mkdir /root/os/host for this to work)
Remounts /tmp
Dumps the arguments and environment to the log (not much there, but was inserted for debugging)
Switches the error and standard output streams for this process to /var/log/initos.log
Runs the injection located at: /root/injection (on the destination, after the changeroot, aka /root/os/root/injection)
Reloads the targeted init, replacing it's own process's text, data, bss, and stack segment.

The first injection allows me to run a copy of sshd which is rooted within the host OS. Effectively allowing me to shell into the host OS. I run this injection asynchronously. This could be more elegant, but it does the job.

#!/bin/sh
#
# /sbin/injection

# Chain to async
/sbin/injection-async &

That script just fires up the following script asyncronously:

#!/bin/sh
#
# /sbin/injection-async

# Give the init time to hand things off, this isn't required, but I like it
sleep 10s

# Ensure network is up
ifconfig venet0:0 up
ifconfig venet0:1 up

# Start sshd
# This is the manual way, ensure the privilege separation folder exists
if [ ! -d /var/run/sshd ]; then
        mkdir /var/run/sshd
        chmod 0755 /var/run/sshd
fi

/usr/sbin/sshd -p 1234 -o PIDFILE=/var/run/host-ssh

The second injection file, within the destination OS, is used to make corrections to allow loading a foreign distribution. In my case that was Gentoo. I did get that working, but I will cover that in another post. For my host, the Virtuozzo container expects a file in the root directory called reboot. I used the second injection file for that:

#!/bin/sh
#
# /root/injection

# fix rebootability
cp /reboot /host

Remember that you must set the execution bit of all of these scripts:

chmod a+x /sbin/injection
chmod a+x /sbin/injection-async
chmod a+x /root/os/root/injection

All is said and done, I reboot, and find that I can indeed shell to host OS ssh -p1234 myhost.dom, and can shell into new distro: ssh myhost.dom. Excellent.

One final test, I run an apt-get update and an apt-get upgrade. Yay! I’m effectively running two OS’s (shared process space) on my VPS. And life is good. Now my entire OS lives in one folder and I can back that up quickly, and if I ever again need to reinstall my (host) OS from the template using the control panel, it will be a short step to being right back where I started.

© 2010 cjsbox.net / blog Suffusion theme by Sayontan Sinha