diff --git a/daemon/daemon.h b/daemon/daemon.h
index b77d764..6535658 100644
--- a/daemon/daemon.h
+++ b/daemon/daemon.h
@@ -231,6 +231,9 @@ extern void journal_finalize (void);
 /*-- in proto.c --*/
 extern void main_loop (int sock) __attribute__((noreturn));
 
+/*-- in xattr.c --*/
+extern int copy_xattrs (const char *src, const char *dest);
+
 /* ordinary daemon functions use these to indicate errors
  * NB: you don't need to prefix the string with the current command,
  * it is added automatically by the client-side RPC stubs.
diff --git a/daemon/file.c b/daemon/file.c
index f348f87..34ec95a 100644
--- a/daemon/file.c
+++ b/daemon/file.c
@@ -28,6 +28,7 @@
 #include "guestfs_protocol.h"
 #include "daemon.h"
 #include "actions.h"
+#include "optgroups.h"
 
 GUESTFSD_EXT_CMD(str_file, file);
 GUESTFSD_EXT_CMD(str_zcat, zcat);
@@ -584,3 +585,79 @@ do_filesize (const char *path)
 
   return buf.st_size;
 }
+
+int
+do_copy_attributes (const char *src, const char *dest, int all, int mode, int xattributes, int ownership)
+{
+  int r;
+  struct stat srcstat, deststat;
+
+  static const unsigned int file_mask = 07777;
+
+  /* If it was specified to copy everything, manually enable all the flags
+   * not manually specified to avoid checking for flag || all everytime.
+   */
+  if (all) {
+    if (!(optargs_bitmask & GUESTFS_COPY_ATTRIBUTES_MODE_BITMASK))
+      mode = 1;
+    if (!(optargs_bitmask & GUESTFS_COPY_ATTRIBUTES_XATTRIBUTES_BITMASK))
+      xattributes = 1;
+    if (!(optargs_bitmask & GUESTFS_COPY_ATTRIBUTES_OWNERSHIP_BITMASK))
+      ownership = 1;
+  } else if ((!(optargs_bitmask & GUESTFS_COPY_ATTRIBUTES_ALL_BITMASK)) &&
+             (!(optargs_bitmask & GUESTFS_COPY_ATTRIBUTES_MODE_BITMASK))) {
+    /* Neither "all" nor "mode" were specified, so make "mode" on by default.
+     */
+    mode = 1;
+  }
+
+  CHROOT_IN;
+  r = stat (src, &srcstat);
+  CHROOT_OUT;
+
+  if (r == -1) {
+    reply_with_perror ("stat: %s", src);
+    return -1;
+  }
+
+  CHROOT_IN;
+  r = stat (dest, &deststat);
+  CHROOT_OUT;
+
+  if (r == -1) {
+    reply_with_perror ("stat: %s", dest);
+    return -1;
+  }
+
+  if (mode &&
+      ((srcstat.st_mode & file_mask) != (deststat.st_mode & file_mask))) {
+    CHROOT_IN;
+    r = chmod (dest, (srcstat.st_mode & file_mask));
+    CHROOT_OUT;
+
+    if (r == -1) {
+      reply_with_perror ("chmod: %s", dest);
+      return -1;
+    }
+  }
+
+  if (ownership &&
+      (srcstat.st_uid != deststat.st_uid || srcstat.st_gid != deststat.st_gid)) {
+    CHROOT_IN;
+    r = chown (dest, srcstat.st_uid, srcstat.st_gid);
+    CHROOT_OUT;
+
+    if (r == -1) {
+      reply_with_perror ("chown: %s", dest);
+      return -1;
+    }
+  }
+
+  if (xattributes && optgroup_linuxxattrs_available ()) {
+    if (!copy_xattrs (src, dest))
+      /* copy_xattrs replies with an error already. */
+      return -1;
+  }
+
+  return 0;
+}
diff --git a/daemon/xattr.c b/daemon/xattr.c
index e01e9e2..08c09e1 100644
--- a/daemon/xattr.c
+++ b/daemon/xattr.c
@@ -542,8 +542,77 @@ do_lgetxattr (const char *path, const char *name, size_t *size_r)
   return buf; /* caller frees */
 }
 
+int
+copy_xattrs (const char *src, const char *dest)
+{
+  ssize_t len, vlen, ret;
+  CLEANUP_FREE char *buf = NULL, *attrval = NULL;
+  size_t i, attrval_len = 0;
+
+  buf = _listxattrs (src, listxattr, &len);
+  if (buf == NULL)
+    /* _listxattrs issues reply_with_perror already. */
+    goto error;
+
+  /* What we get from the kernel is a string "foo\0bar\0baz" of length
+   * len.
+   */
+  for (i = 0; i < (size_t) len; i += strlen (&buf[i]) + 1) {
+    CHROOT_IN;
+    vlen = getxattr (src, &buf[i], NULL, 0);
+    CHROOT_OUT;
+    if (vlen == -1) {
+      reply_with_perror ("getxattr: %s, %s", src, &buf[i]);
+      goto error;
+    }
+
+    if (vlen > XATTR_SIZE_MAX) {
+      /* The next call to getxattr will fail anyway, so ... */
+      reply_with_error ("extended attribute is too large");
+      goto error;
+    }
+
+    if (vlen > attrval_len) {
+      char *new = realloc (attrval, vlen);
+      if (new == NULL) {
+        reply_with_perror ("realloc");
+        goto error;
+      }
+      attrval = new;
+      attrval_len = vlen;
+    }
+
+    CHROOT_IN;
+    vlen = getxattr (src, &buf[i], attrval, vlen);
+    CHROOT_OUT;
+    if (vlen == -1) {
+      reply_with_perror ("getxattr: %s, %s", src, &buf[i]);
+      goto error;
+    }
+
+    CHROOT_IN;
+    ret = setxattr (dest, &buf[i], attrval, vlen, 0);
+    CHROOT_OUT;
+    if (vlen == -1) {
+      reply_with_perror ("setxattr: %s, %s", dest, &buf[i]);
+      goto error;
+    }
+  }
+
+  return 1;
+
+ error:
+  return 0;
+}
+
 #else /* no HAVE_LINUX_XATTRS */
 
 OPTGROUP_LINUXXATTRS_NOT_AVAILABLE
 
+int
+copy_xattrs (const char *src, const char *dest)
+{
+  abort ();
+}
+
 #endif /* no HAVE_LINUX_XATTRS */
diff --git a/fish/Makefile.am b/fish/Makefile.am
index bd0485f..fb0e99e 100644
--- a/fish/Makefile.am
+++ b/fish/Makefile.am
@@ -279,6 +279,7 @@ if ENABLE_APPLIANCE
 TESTS += \
 	test-copy.sh \
 	test-edit.sh \
+	test-file-attrs.sh \
 	test-find0.sh \
 	test-inspect.sh \
 	test-glob.sh \
diff --git a/fish/test-file-attrs.sh b/fish/test-file-attrs.sh
new file mode 100755
index 0000000..b85a35c
--- /dev/null
+++ b/fish/test-file-attrs.sh
@@ -0,0 +1,157 @@
+#!/bin/bash -
+# libguestfs
+# Copyright (C) 2014 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# Test guestfish file attributes commands (chmod, copy-attributes, etc).
+
+set -e
+export LANG=C
+
+rm -f test.out
+
+$VG ./guestfish > test.out <<EOF
+scratch 50MB
+run
+part-disk /dev/sda mbr
+mkfs ext2 /dev/sda1
+mount /dev/sda1 /
+
+touch /foo
+touch /bar
+chmod 0712 /foo
+stat /foo | grep mode:
+copy-attributes /foo /bar
+stat /bar | grep mode:
+
+echo -----
+
+stat /foo | grep uid:
+stat /foo | grep gid:
+chown 10 11 /foo
+stat /foo | grep uid:
+stat /foo | grep gid:
+stat /bar | grep uid:
+stat /bar | grep gid:
+copy-attributes /foo /bar ownership:true
+stat /bar | grep uid:
+stat /bar | grep gid:
+
+echo -----
+
+setxattr user.test foo 3 /foo
+setxattr user.test2 secondtest 10 /foo
+setxattr user.foo another 7 /bar
+lxattrlist / "foo bar"
+copy-attributes /foo /bar xattributes:true
+lxattrlist / "foo bar"
+
+echo -----
+
+touch /new
+chmod 0111 /new
+copy-attributes /foo /new all:true mode:false
+stat /new | grep mode:
+stat /new | grep uid:
+stat /new | grep gid:
+lxattrlist / new
+copy-attributes /foo /new mode:true
+stat /new | grep mode:
+EOF
+
+if [ "$(cat test.out)" != "mode: 33226
+mode: 33226
+-----
+uid: 0
+gid: 0
+uid: 10
+gid: 11
+uid: 0
+gid: 0
+uid: 10
+gid: 11
+-----
+[0] = {
+  attrname: 
+  attrval: 2\x00
+}
+[1] = {
+  attrname: user.test
+  attrval: foo
+}
+[2] = {
+  attrname: user.test2
+  attrval: secondtest
+}
+[3] = {
+  attrname: 
+  attrval: 1\x00
+}
+[4] = {
+  attrname: user.foo
+  attrval: another
+}
+[0] = {
+  attrname: 
+  attrval: 2\x00
+}
+[1] = {
+  attrname: user.test
+  attrval: foo
+}
+[2] = {
+  attrname: user.test2
+  attrval: secondtest
+}
+[3] = {
+  attrname: 
+  attrval: 3\x00
+}
+[4] = {
+  attrname: user.foo
+  attrval: another
+}
+[5] = {
+  attrname: user.test
+  attrval: foo
+}
+[6] = {
+  attrname: user.test2
+  attrval: secondtest
+}
+-----
+mode: 32841
+uid: 10
+gid: 11
+[0] = {
+  attrname: 
+  attrval: 2\x00
+}
+[1] = {
+  attrname: user.test
+  attrval: foo
+}
+[2] = {
+  attrname: user.test2
+  attrval: secondtest
+}
+mode: 33226" ]; then
+    echo "$0: unexpected output:"
+    cat test.out
+    exit 1
+fi
+
+rm test.out
diff --git a/generator/actions.ml b/generator/actions.ml
index 9fa7acb..5139f22 100644
--- a/generator/actions.ml
+++ b/generator/actions.ml
@@ -11647,6 +11647,44 @@ This function is used internally when setting up the appliance." };
 This function is used internally when closing the appliance.  Note
 it's only called when ./configure --enable-valgrind-daemon is used." };
 
+  { defaults with
+    name = "copy_attributes";
+    style = RErr, [Pathname "src"; Pathname "dest"], [OBool "all"; OBool "mode"; OBool "xattributes"; OBool "ownership"];
+    proc_nr = Some 415;
+    shortdesc = "copy the attributes of a path (file/directory) to another";
+    longdesc = "\
+Copy the attributes of a path (which can be a file or a directory)
+to another path.
+
+By default C<mode> is assumed if not explicitly specified, so set it
+(or C<all>) as false to not copy the file mode.
+
+The optional arguments specify which attributes can be copied:
+
+=over 4
+
+=item C<mode>
+
+Copy part of the file mode from C<source> to C<destination>. Only the
+UNIX permissions and the sticky/setuid/setgid bits can be copied.
+
+=item C<xattributes>
+
+Copy the Linux extended attributes (xattrs) from C<source> to C<destination>.
+This flag does nothing if the I<linuxxattrs> feature is not available
+(see C<guestfs_feature_available>).
+
+=item C<ownership>
+
+Copy the owner uid and the group gid of C<source> to C<destination>.
+
+=item C<all>
+
+Copy B<all> the attributes from C<source> to C<destination>. Enabling it
+enables all the other flags, if they are not specified already.
+
+=back" };
+
 ]
 
 (* Non-API meta-commands available only in guestfish.
diff --git a/src/MAX_PROC_NR b/src/MAX_PROC_NR
index d1b9f6a..21c8d99 100644
--- a/src/MAX_PROC_NR
+++ b/src/MAX_PROC_NR
@@ -1 +1 @@
-414
+415
