diff --git a/os/run/chroot.run b/os/run/chroot.run
new file mode 100644
index 0000000000..df01363530
--- /dev/null
+++ b/os/run/chroot.run
@@ -0,0 +1,122 @@
+#
+# \brief Test for using chroot on Linux
+# \author Norman Feske
+# \date 2012-04-18
+#
+#
+if {![have_spec linux]} { puts "Run script requires Linux"; exit 0 }
+
+#
+# Build
+#
+
+build { core init app/chroot drivers/timer/linux test/timer }
+
+if {[catch { exec which setcap }]} {
+ puts stderr "Error: setcap not available, please install the libcap2-bin package"
+ return 0
+}
+
+
+create_boot_directory
+
+#
+# Generate config
+#
+
+set config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+proc chroot_path { } { return "/tmp/chroot-test" }
+proc chroot_cwd_path { } { return "[chroot_path][pwd]/[run_dir]" }
+proc chroot_genode_tmp_path { } { return "[chroot_path]/tmp/genode-[exec id -u]" }
+
+proc cleanup_chroot { } {
+ catch { exec sudo umount -l [chroot_cwd_path] }
+ catch { exec sudo umount -l [chroot_genode_tmp_path] }
+ exec rm -rf [chroot_path]
+}
+
+# replace 'chroot_path' marker in config with actual path
+regsub "chroot_path" $config [chroot_path] config
+
+install_config $config
+
+#
+# Copy boot modules into run directory
+#
+# We cannot use the predefined 'build_boot_image' function here because
+# this would create mere symlinks. However, we want to hardlink the
+# run directory into the chroot environment. If the directory entries
+# were symlinks, those would point to nowhere within the chroot.
+#
+foreach binary { core init chroot timer test-timer } {
+ exec cp -H bin/$binary [run_dir] }
+
+#
+# Grant chroot permission to 'chroot' tool
+#
+# CAP_SYS_ADMIN is needed for bind mounting genode runtime directories
+# CAP_SYS_CHROOT is needed to perform the chroot syscall
+#
+exec sudo setcap cap_sys_admin,cap_sys_chroot=ep [run_dir]/chroot
+
+#
+# Setup chroot environment
+#
+
+# start with fresh directory
+cleanup_chroot
+exec mkdir -p [chroot_path]
+
+#
+# Execute test case
+#
+run_genode_until {.*--- timer test finished ---.*} 60
+
+#
+# Remove artifacts created while running the test
+#
+cleanup_chroot
+
+# vi: set ft=tcl :
diff --git a/os/src/app/chroot/main.cc b/os/src/app/chroot/main.cc
new file mode 100644
index 0000000000..f28a6add49
--- /dev/null
+++ b/os/src/app/chroot/main.cc
@@ -0,0 +1,211 @@
+/*
+ * \brief Utility for using the Linux chroot mechanism with Genode
+ * \author Norman Feske
+ * \date 2012-04-18
+ */
+
+/* Genode includes */
+#include
+#include
+
+/* Linux includes */
+#include
+#include
+#include
+#include
+#include
+
+
+enum { MAX_PATH_LEN = 256 };
+
+static bool verbose = false;
+
+
+/**
+ * Return true if specified path is an existing directory
+ */
+static bool is_directory(char const *path)
+{
+ struct stat s;
+ if (stat(path, &s) != 0)
+ return false;
+
+ if (!(s.st_mode & S_IFDIR))
+ return false;
+
+ return true;
+}
+
+
+static bool is_path_delimiter(char c) { return c == '/'; }
+
+
+static bool has_trailing_path_delimiter(char const *path)
+{
+ char last_char = 0;
+ for (; *path; path++)
+ last_char = *path;
+
+ return is_path_delimiter(last_char);
+}
+
+
+/**
+ * Return number of path elements of given path
+ */
+static size_t num_path_elements(char const *path)
+{
+ size_t count = 0;
+
+ /*
+ * If path starts with non-slash, the first characters belongs to a path
+ * element.
+ */
+ if (*path && !is_path_delimiter(*path))
+ count = 1;
+
+ /* count slashes */
+ for (; *path; path++)
+ if (is_path_delimiter(*path))
+ count++;
+
+ return count;
+}
+
+
+static bool leading_path_elements(char const *path, unsigned num,
+ char *dst, size_t dst_len)
+{
+ /* counter of path delimiters */
+ unsigned count = 0;
+ unsigned i = 0;
+
+ if (is_path_delimiter(path[0]))
+ num++;
+
+ for (; path[i] && (count < num) && (i < dst_len); i++)
+ {
+ if (is_path_delimiter(path[i]))
+ count++;
+
+ if (count == num)
+ break;
+
+ dst[i] = path[i];
+ }
+
+ if (i + 1 < dst_len) {
+ dst[i] = 0;
+ return true;
+ }
+
+ /* string is cut, append null termination anyway */
+ dst[dst_len - 1] = 0;
+ return false;
+}
+
+
+static void mirror_path_to_chroot(char const *chroot_path, char const *path)
+{
+ char target_path[MAX_PATH_LEN];
+ Genode::snprintf(target_path, sizeof(target_path), "%s%s",
+ chroot_path, path);
+
+ /*
+ * Create directory hierarchy pointing to the target path except for the
+ * last element. The last element will be bind-mounted to refer to the
+ * original 'path'.
+ */
+ for (unsigned i = 1; i <= num_path_elements(target_path); i++)
+ {
+ char buf[MAX_PATH_LEN];
+ leading_path_elements(target_path, i, buf, sizeof(buf));
+
+ /* skip existing directories */
+ if (is_directory(buf))
+ continue;
+
+ /* create new directory */
+ mkdir(buf, 0777);
+ }
+
+ umount(target_path);
+
+ if (verbose) {
+ PINF("bind mount from: %s", path);
+ PINF(" to: %s", target_path);
+ }
+
+ if (mount(path, target_path, 0, MS_BIND, 0))
+ PERR("bind mount failed (errno=%d: %s)", errno, strerror(errno));
+}
+
+
+int main(int, char **argv)
+{
+ using namespace Genode;
+
+ static char chroot_path[MAX_PATH_LEN];
+ static char cwd_path[MAX_PATH_LEN];
+ static char genode_tmp_path[MAX_PATH_LEN];
+
+ /*
+ * Read configuration
+ */
+ try {
+ Xml_node config = Genode::config()->xml_node();
+
+ config.sub_node("root").attribute("path")
+ .value(chroot_path, sizeof(chroot_path));
+
+ verbose = config.attribute("verbose").has_value("yes");
+
+ } catch (...) {
+ PERR("invalid config");
+ return 1;
+ }
+
+ getcwd(cwd_path, sizeof(cwd_path));
+
+ uid_t const uid = getuid();
+ snprintf(genode_tmp_path, sizeof(genode_tmp_path), "/tmp/genode-%d", uid);
+
+ /*
+ * Print diagnostic information
+ */
+ if (verbose) {
+ PINF("work directory: %s", cwd_path);
+ PINF("chroot path: %s", chroot_path);
+ PINF("genode tmp path: %s", genode_tmp_path);
+ }
+
+ /*
+ * Validate chroot path
+ */
+ if (!is_directory(chroot_path)) {
+ PERR("chroot path does not point to valid directory");
+ return 2;
+ }
+
+ if (has_trailing_path_delimiter(chroot_path)) {
+ PERR("chroot path has trailing slash");
+ return 3;
+ }
+
+ /*
+ * Hardlink directories needed for running Genode within the chroot
+ * environment.
+ */
+ mirror_path_to_chroot(chroot_path, cwd_path);
+ mirror_path_to_chroot(chroot_path, genode_tmp_path);
+
+ printf("changing root to %s ...\n", chroot_path);
+
+ if (chroot(chroot_path)) {
+ PERR("chroot failed (errno=%d: %s)", errno, strerror(errno));
+ return 4;
+ }
+
+ execve("init", argv, environ);
+ return 0;
+}
diff --git a/os/src/app/chroot/target.mk b/os/src/app/chroot/target.mk
new file mode 100644
index 0000000000..34f42cc7f7
--- /dev/null
+++ b/os/src/app/chroot/target.mk
@@ -0,0 +1,19 @@
+TARGET = chroot
+REQUIRES = linux
+SRC_CC = main.cc
+LIBS = cxx env server lx_hybrid
+
+#
+# XXX find a way to remove superfluous warning:
+#
+# base/include/util/token.h: In constructor ‘Genode::Config::Config()’:
+# base/include/util/token.h:69:67: warning: ‘ret’ may be used uninitialized in
+# this function [-Wuninitialized]
+# base/include/base/capability.h:196:62: note: ‘ret’ was declared here
+# base/include/util/token.h:100:68: warning: ‘prephitmp.1897’ may be used
+# uninitialized in this function
+# [-Wuninitialized]
+# os/include/os/config.h:42:4: note: ‘prephitmp.1897’ was declared here
+#
+CC_WARN = -Wall -Wno-uninitialized
+