diff --git a/daemon/.gitignore b/daemon/.gitignore
index e06c684..c9837be 100644
--- a/daemon/.gitignore
+++ b/daemon/.gitignore
@@ -50,6 +50,7 @@ m4/gettime.m4
 m4/gettimeofday.m4
 m4/getpagesize.m4
 m4/getugroups.m4
+m4/gl_list.m4
 m4/glibc21.m4
 m4/glob.m4
 m4/gnulib-common.m4
diff --git a/daemon/Makefile.am b/daemon/Makefile.am
index 0e2ac6e..4045600 100644
--- a/daemon/Makefile.am
+++ b/daemon/Makefile.am
@@ -95,6 +95,7 @@ guestfsd_SOURCES = \
 	guestfsd.c \
 	headtail.c \
 	hexdump.c \
+	hfile.c \
 	htonl.c \
 	initrd.c \
 	inotify.c \
diff --git a/daemon/hfile.c b/daemon/hfile.c
new file mode 100644
index 0000000..d9d5755
--- /dev/null
+++ b/daemon/hfile.c
@@ -0,0 +1,249 @@
+/* libguestfs - the guestfsd daemon
+ * Copyright (C) 2010 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+#include <config.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "gl_array_list.h"
+
+#include "daemon.h"
+#include "actions.h"
+
+#ifndef O_CLOEXEC
+  #define O_CLOEXEC 0
+#endif
+
+gl_list_t handles;
+int64_t hread_message_max;
+
+static void init_hfile (void) __attribute__((constructor));
+void
+init_hfile (void)
+{
+    /* Initialise the handles array */
+    handles = gl_list_nx_create_empty(GL_ARRAY_LIST, NULL, NULL, NULL, false);
+
+    /* Calculate the maximum amount of data which hread can return.
+     * In the absence of a sane API, we do this by writting header information
+     * to an XDR stream and measuring how far the stream moved. */
+    char buffer[1024];
+    XDR xdr;
+    xdrmem_create(&xdr, buffer, sizeof(buffer), XDR_ENCODE);
+
+    struct guestfs_message_header hdr;
+    if (!xdr_guestfs_message_header(&xdr, &hdr)) {
+        fprintf(stderr, "guestfsd: failed to encode message header\n");
+        exit(EXIT_FAILURE);
+    }
+
+    struct guestfs_hread_args args;
+    if (!xdr_guestfs_hread_args(&xdr, &args)) {
+        fprintf(stderr, "guestfsd: failed to encode hread args\n");
+        exit(EXIT_FAILURE);
+    }
+
+    hread_message_max = GUESTFS_MESSAGE_MAX - xdr_getpos(&xdr);
+
+    xdr_destroy(&xdr);
+}
+
+static gl_list_node_t
+get_handle (int handle)
+{
+    gl_list_node_t node = gl_list_search(handles, (void *)(long)handle);
+    if (NULL == node) {
+        reply_with_error("%i does not refer to an open handle", handle);
+        return NULL;
+    }
+
+    return node;
+}
+
+static int
+hopen_check(int fd, const char *path)
+{
+    if (fd == -1) {
+        reply_with_perror("open %s failed", path);
+        return -1;
+    }
+
+    gl_list_node_t node = gl_list_nx_add_last(handles, (void *)(long)fd);
+    if (NULL == node) {
+        close(fd);
+
+        reply_with_error("gl_list_nx_add_last");
+        return -1;
+    }
+
+    return fd;
+}
+
+int /* RInt */
+do_hopen_file (const char *path, int flags)
+{
+    int fd;
+
+    /* Don't leak descriptors to other child processes */
+    flags |= O_CLOEXEC;
+
+    CHROOT_IN;
+    if (flags && O_CREAT) {
+        fd = open(path, flags, 0777);
+    } else {
+        fd = open(path, flags);
+    }
+    CHROOT_OUT;
+
+    return hopen_check(fd, path);
+}
+
+int /* RInt */
+do_hopen_device (const char *device, int flags)
+{
+    int fd;
+
+    /* Don't leak descriptors to other child processes */
+    flags |= O_CLOEXEC;
+
+    fd = open(device, flags);
+
+    return hopen_check(fd, device);
+}
+
+int /* RErr */
+do_hclose (int handle)
+{
+    gl_list_node_t node = get_handle(handle);
+    if (NULL == node) {
+        return -1;
+    }
+
+    gl_list_remove_node(handles, node);
+
+    if (close(handle) < 0) {
+        reply_with_perror("close handle %i failed", handle);
+        return -1;
+    }
+
+    return 0;
+}
+
+int /* RErr */
+do_hclose_all (void)
+{
+    const void *eltp;
+    gl_list_node_t node;
+    gl_list_iterator_t i = gl_list_iterator(handles);
+
+    char errors[GUESTFS_ERROR_LEN];
+    char *error_p = errors;
+    size_t error_n = sizeof(errors);
+
+    bool failed = false;
+    while (gl_list_iterator_next(&i, &eltp, &node)) {
+        gl_list_remove_node(handles, node);
+
+        int fd = (long)eltp;
+        if (close(fd) < 0) {
+            failed = true;
+
+            if (error_n != 0) {
+                size_t len = snprintf(error_p, error_n,
+                                      "close handle %i failed: %m ", fd);
+                if (len < error_n) {
+                    error_p += len;
+                    error_n -= len;
+                } else {
+                    error_n = 0;
+                    error_p = &errors[GUESTFS_ERROR_LEN];
+                }
+            }
+        }
+    }
+
+    if (failed) {
+        /* Overwrite the trailing space with a NULL terminator */
+        *(error_p - 1) = '\0';
+        reply_with_error("%s", errors);
+        return -1;
+    }
+
+    return 0;
+}
+
+char * /* RBufferOut */
+do_hread (int handle, int64_t size, size_t *size_r)
+{
+    if (get_handle(handle) == NULL) {
+        return NULL;
+    }
+
+    if (size > hread_message_max) {
+        size = hread_message_max;
+    }
+
+    char *buf = malloc(size);
+    if (NULL == buf) {
+        reply_with_perror("malloc");
+        return NULL;
+    }
+
+    ssize_t in = read(handle, buf, size);
+    if (in < 0) {
+        reply_with_perror("error reading from handle %i", handle);
+        free(buf);
+        return NULL;
+    }
+
+    *size_r = in;
+    return buf;
+}
+
+int /* RErr */
+do_hwrite (int handle, const char *content, size_t content_size)
+{
+    if (get_handle(handle) == NULL) {
+        return -1;
+    }
+
+    if (xwrite(handle, content, content_size) == -1) {
+        reply_with_perror("error writing to handle %i", handle);
+        return -1;
+    }
+
+    return 0;
+}
+
+int64_t /* RInt64 */
+do_hseek (int handle, int64_t offset, int whence)
+{
+    if (get_handle(handle) == NULL) {
+        return -1;
+    }
+
+    int64_t ret = lseek(handle, offset, whence);
+    if (ret < 0) {
+        reply_with_perror("error seeking in handle %i", handle);
+        return -1;
+    }
+
+    return ret;
+}
diff --git a/daemon/m4/gnulib-cache.m4 b/daemon/m4/gnulib-cache.m4
index f026cb3..25ffd27 100644
--- a/daemon/m4/gnulib-cache.m4
+++ b/daemon/m4/gnulib-cache.m4
@@ -15,11 +15,12 @@
 
 
 # Specification in the form of a command-line invocation:
-#   gnulib-tool --import --dir=. --lib=libgnu --source-base=lib --m4-base=m4 --doc-base=doc --tests-base=tests --aux-dir=build-aux --with-tests --no-libtool --macro-prefix=gl byteswap c-ctype connect error fsusage futimens getaddrinfo getline glob hash ignore-value manywarnings mkdtemp netdb openat perror pread read-file readlink select sleep socket strchrnul strndup symlinkat sys_select sys_wait vasprintf warnings
+#   gnulib-tool --import --dir=. --lib=libgnu --source-base=lib --m4-base=m4 --doc-base=doc --tests-base=tests --aux-dir=build-aux --with-tests --no-libtool --macro-prefix=gl array-list byteswap c-ctype connect error fsusage futimens getaddrinfo getline glob hash ignore-value manywarnings mkdtemp netdb openat perror pread read-file readlink select sleep socket strchrnul strndup symlinkat sys_select sys_wait vasprintf warnings
 
 # Specification in the form of a few gnulib-tool.m4 macro invocations:
 gl_LOCAL_DIR([])
 gl_MODULES([
+  array-list
   byteswap
   c-ctype
   connect
diff --git a/generator/generator_actions.ml b/generator/generator_actions.ml
index d01871f..67d83ba 100644
--- a/generator/generator_actions.ml
+++ b/generator/generator_actions.ml
@@ -5117,6 +5117,120 @@ removes the partition number, returning the device name
 The named partition must exist, for example as a string returned
 from C<guestfs_list_partitions>.");
 
+  ("hopen_file", (RInt "handle", [Pathname "path"; Int "flags"]), 273, [],
+   [],
+   "open a file",
+   "\
+This command opens C<path> in the guest and returns a handle
+to it.  The returned handle is an opaque int which can be passed
+to C<guestfs_h*> calls.
+
+C<flags> determines how the file is opened. It is a bitwise or of:
+
+=over 4
+
+=item 0
+
+(O_RDONLY) Open the file read only.
+
+=item 1
+
+(O_WRONLY) Open the file write only.
+
+=item 2
+
+(O_RDWR) Open the file read-write.
+
+=item 64
+
+(O_CREAT) Create the file if it does not already exist.  The file
+will be created with a mode of 0777, modified by the daemon's
+umask.  See L<guestfs(3)/UMASK>.
+
+=item 512
+
+(O_TRUNC) Truncate a file opened for write to zero length when
+opening.
+
+=item 1024
+
+(O_APPEND) Always write to the end of the file.
+
+=item 262144
+
+(O_NOATIME) Don't update the access time when reading from a file.
+
+=back 4");
+
+  ("hopen_device", (RInt "handle", [Device "device"; Int "flags"]), 274, [],
+   [],
+   "open a block device",
+   "\
+This command opens a block device and returns a handle to it.
+The returned handle is an opaque int which can be passed to
+C<guestfs_h*> calls.
+
+See C<guestfs_hopen_file> for a description of C<flags>.");
+
+  ("hclose", (RErr, [Int "handle"]), 275, [],
+   [],
+   "close a file handle",
+   "\
+This command closes C<handle> and releases all resources associated
+with it.");
+
+  ("hclose_all", (RErr, []), 276, [],
+   [],
+   "close all open file handles",
+   "\
+This command closes all file handles which have been opened with
+C<guestfs_hopen_*>.");
+
+  ("hread", (RBufferOut "content", [Int "handle"; Int64 "length"]), 277,
+  [ProtocolLimitWarning], [],
+   "read data from an open handle",
+   "\
+This command reads up to C<length> bytes from C<handle> and returns
+the data in C<content>.  It will attempt to read exactly C<length>
+bytes, but may return less. Returned C<content> with size 0
+indicates EOF.");
+
+  ("hwrite", (RErr, [Int "handle"; BufferIn "content"]), 278,
+   [ProtocolLimitWarning], [],
+   "write data to an open handle",
+   "\
+This command writes all the data in C<content> to C<handle>.");
+
+  ("hseek", (RInt64 "newoffset", [Int "handle"; Int64 "offset";
+                                  Int "whence"]), 279, [],
+   [],
+   "seek on an open handle",
+   "\
+This command updates the current position in C<handle>.  This
+affects the C<guestfs_hread> and C<guestfs_hwrite> calls.
+
+C<whence> determines the point of reference for C<offset>.  It
+must be one of:
+
+=over 4
+
+=item 0
+
+C<offset> is relative to the beginning of the file.
+
+=item 1
+
+C<offset> is relative to the current position.
+
+=item 2
+
+C<offset> is relative to the end of the file.
+
+=back 4
+
+hseek returns the new offset from the beginning of the file.");
+
+
 ]
 
 let all_functions = non_daemon_functions @ daemon_functions
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 4518eab..24b3970 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -29,6 +29,7 @@ daemon/grub.c
 daemon/guestfsd.c
 daemon/headtail.c
 daemon/hexdump.c
+daemon/hfile.c
 daemon/htonl.c
 daemon/initrd.c
 daemon/inotify.c
@@ -109,6 +110,7 @@ perl/lib/Sys/Guestfs/Lib.pm
 php/extension/guestfs_php.c
 python/guestfs-py.c
 regressions/rhbz501893.c
+regressions/test-hfile.pl
 regressions/test-lvm-mapping.pl
 regressions/test-noexec-stack.pl
 ruby/ext/guestfs/_guestfs.c
diff --git a/regressions/Makefile.am b/regressions/Makefile.am
index 3a6fe4d..599652f 100644
--- a/regressions/Makefile.am
+++ b/regressions/Makefile.am
@@ -36,6 +36,7 @@ TESTS = \
 	test-cancellation-upload-daemoncancels.sh \
 	test-copy.sh \
 	test-find0.sh \
+	test-hfile.pl \
 	test-luks.sh \
 	test-lvm-filtering.sh \
 	test-lvm-mapping.pl \
diff --git a/regressions/test-hfile.pl b/regressions/test-hfile.pl
new file mode 100755
index 0000000..1c52169
--- /dev/null
+++ b/regressions/test-hfile.pl
@@ -0,0 +1,236 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use File::Temp qw(tempfile);
+use Sys::Guestfs;
+
+# Defined in src/generator.ml
+use constant GUESTFS_MESSAGE_MAX => 4 * 1024 * 1024;
+
+# Create a new guestfs handle
+my $g = Sys::Guestfs->new();
+
+# Initialise the guestfs handle with a 10M image file
+{
+    my $testimg = "test.img";
+    unlink($testimg);
+
+    my $fh;
+    open($fh, '>', $testimg) or die("Open $testimg: $!");
+    truncate($fh, 1024*1024*10) or die("truncate $testimg: $!");
+    close($fh) or die("close $testimg: $!");
+
+    $g->add_drive($testimg);
+    $g->launch();
+
+    # As qemu is now running we can unlink the image
+    unlink($testimg) or die("unlink $testimg failed: $!");
+}
+
+my $string = "abcd";
+
+# Check we can't read or write handles we haven't opened
+{
+    # Read from guestfsd's STDIN
+    eval { my $in = $g->hread(0, 4); };
+    die("read guestfsd's STDIN") unless ($@);
+
+    # Write to guestfsd's STDOUT
+    eval { $g->hwrite(1, "test"); };
+    die("write guestfsd's STDOUT") unless ($@);
+}
+
+{
+    # Test read and writing directly to a block device
+    my $h = $g->hopen_device("/dev/vda", 2);
+
+    $g->hwrite($h, $string);
+    $g->hseek($h, 0, 0);
+    my $in = $g->hread($h, length($string));
+    die("device: hread returned $in") unless ($in eq $string);
+
+    # Check we can't seek past the end of the file
+    eval { $g->hseek($h, 1024*1024*11, 0); };
+    die("device: seek past EOD") unless ($@);
+
+    # Check we can't write past the end of the file
+    $g->hseek($h, 0, 2);
+    eval { $g->hwrite($h, "test"); };
+    die("device: write past EOD") unless ($@);
+
+    $g->hclose($h);
+
+    # Check $h is no longer valid
+    eval {
+        $g->hseek($h, 0, 0);
+    };
+    die("device: handle wasn't closed") unless($@);
+}
+
+# Make an ext2 filesystem and mount it
+$g->mkfs("ext2", "/dev/vda");
+$g->mount("/dev/vda", "/");
+
+# Open should fail read-only
+eval {
+    my $h = $g->hopen_file("/test", 0);
+};
+die("file: read-only open non-existant file") unless($@);
+
+# Open should fail write-only without O_CREAT
+eval {
+    my $h = $g->hopen_file("/test", 1);
+};
+die("file: write-only open non-existant file") unless($@);
+
+# Open should fail read-write without O_CREAT
+eval {
+    my $h = $g->hopen_file("/test", 2);
+};
+die("file: read-write open non-existant file") unless($@);
+
+{
+    # Open the file write only, and create it
+    my $h = $g->hopen_file("/test", 1 | 64);
+    my $in;
+
+    # Write data to it
+    $g->hwrite($h, $string);
+
+    # Verify we can't read from it
+    $g->hseek($h, 0, 0);
+    eval {
+        $in = $g->hread($h, length($string));
+    };
+    die("file: read of write-only file") unless($@);
+
+    # close and check the contents with cat
+    $g->hclose($h);
+    $in = $g->cat("/test");
+    die("file: hwrite data test") unless($in eq $string);
+}
+
+{
+    # Open the file read-only
+    my $h = $g->hopen_file("/test", 0);
+
+    # Read the file in 1 byte chunks until the end
+    my $in = "";
+    my $chunk;
+    do {
+        $chunk = $g->hread($h, 1);
+        $in .= $chunk;
+    } until(length($chunk) == 0);
+    die("file: hread in chunks") unless($in eq $string);
+
+    # Read past EOF returns a short read
+    $g->hseek($h, 0, 0);
+    $in = $g->hread($h, length($string) + 1);
+    die("file: read past EOF") unless($in eq $string);
+
+    # Test close-all
+    $g->hclose_all();
+    eval {
+        $g->hseek($h, 0, 0);
+    };
+    die("file: close-all") unless($@);
+}
+
+{
+    # Open the file read-write
+    my $h = $g->hopen_file("/test", 2);
+
+    # Create a list of the first 2M ints
+    my @ints;
+    for (my $i = 0; $i < 1024 * 1024 * 2; $i++) {
+        push(@ints, $i);
+    }
+
+    # Write 2M of data (512k ints)
+    $g->hwrite($h, pack("L".512 * 1024, @ints));
+
+    # Check the file size
+    my $size = $g->filesize("/test");
+    die("file: 2M write wrote $size") unless($size == 1024 * 1024 * 2);
+
+    # Write another 3M of data (768k ints) for the next test
+    $g->hwrite($h, pack("L".768 * 1024, @ints[512*1024..(512+768)*1024]));
+
+    # Check the file size is now 5M for good measure
+    $size = $g->filesize("/test");
+    die("file: 2M + 3M append wrote $size") unless($size == 1024 * 1024 * 5);
+
+    my $bytes;
+    my @data;
+
+    # Check that read 2M works
+    $g->hseek($h, 0, 0);
+    $bytes = $g->hread($h, 1024*1024*2);
+    @data = unpack("L*", $bytes);
+    die("file: read 2M") unless(scalar(@data) == 1024 * 512 && 
+                                $data[$#data] == $#data);
+
+    # Check that read 4M-1 works
+    $g->hseek($h, 0, 0);
+    $bytes = $g->hread($h, 1024*1024*4-1);
+    @data = unpack("L*", $bytes);
+    die("file: read 4M-1") unless($data[$#data] == $#data);
+
+    # Check that read 4M works
+    $g->hseek($h, 0, 0);
+    $bytes = $g->hread($h, 1024*1024*4);
+    @data = unpack("L*", $bytes);
+    die("file: read 4M") unless($data[$#data] == $#data);
+
+    # Check that we can read 5M in 2 3M max chunks
+    $g->hseek($h, 0, 0);
+    $bytes = $g->hread($h, 1024*1024*3-1); # Misalign the data for kicks
+    $bytes .= $g->hread($h, 1024*1024*3-1);
+    @data = unpack("L*", $bytes);
+    die("file: chunked read") unless($#data == (512+768)*1024-1 &&
+                                     $data[$#data] == $#data);
+
+    $g->hclose_all();
+}
+
+{
+    my $forwards = "abcdefghijklmnopqrstuvwxyz";
+    my $backwards = "zyxwvutsrqponmlkjihgfedcba";
+
+    # Create 2 files with different patterns
+    $g->fill_pattern($forwards, 1024, "/forwards");
+    $g->fill_pattern($backwards, 1024, "/backwards");
+
+    # Open 2 handles
+    my $hf = $g->hopen_file("/forwards", 0);
+    my $hb = $g->hopen_file("/backwards", 0);
+
+    # Read alternately from the 2 handles, ensuring the data remains separate
+    my $inf = 0;
+    my $inb = 0;
+    while ($inf < 1024 || $inb < 1024) {
+        my $error = 0;
+
+        if ($inf < 1024) {
+            my $test = $g->hread($hf, 5);
+            if (substr($test, 0, 1) eq substr($forwards, $inf % 26, 1)) {
+                $inf += length($test);
+            } else {
+                $error = 1;
+            }
+        }
+
+        if ($inb < 1024) {
+            my $test = $g->hread($hb, 5);
+            if (substr($test, 0, 1) eq substr($backwards, $inb % 26, 1)) {
+                $inb += length($test);
+            } else {
+                $error = 1;
+            }
+        }
+
+        die("2 handles") if ($error);
+    }
+}
diff --git a/src/MAX_PROC_NR b/src/MAX_PROC_NR
index 31e9cf9..bbb81cf 100644
--- a/src/MAX_PROC_NR
+++ b/src/MAX_PROC_NR
@@ -1 +1 @@
-272
+279
