[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

Re: [libvirt] [PATCHv2 1/2] util: refactor virFileOpenAs

On 01/26/2012 07:33 PM, Eric Blake wrote:
On 01/17/2012 06:44 PM, Laine Stump wrote:
* Reduce what is executed in the context of the child process to just
   the bare minimum necessary to open the file and send its fd back to
   the parent. In particular, the older version had also done fchmod()
Hmm.  I'm not familiar enough with root-squash to know the answer
off-hand.  I know that root-squash prevents uid 0 from open()ing a
remote file (by instead opening the file under the nobody account), but
once the file is open, can root proceed to fchown() or even fchmod() the
file?  fchmod() is unlikely to succeed except in the case of swapping
the gid_t ownership if done as non-root, so I can understand pulling
fchown() out to the public function after the child has already passed
the fd.  And hopefully root can fchmod() an fd that it obtained from a
child; but if not, then you have to move the fchmod() back into the
child code.

I had ASSumed too much, and was wrong. I just artificially created a situation where I would need to fchmod and it failed. So, as you say, I need to move the fchmod() back into the child process code.

   (and had code to call fchown(), although that would never actually
   be executed in the case of forking prior to the open). This code is
   now in the main public function, and also executed in the context of
   the parent process, thus reducing the chance that we may end up
   trying to call some async-unsafe function from the child.
fchown and fchmod are async-safe.  (Most syscalls in 'man 2' fall in
this category; it's when you get to 'man 3' that you are likely to be
introducing library functions that hold locks and aren't atomic where
you are no longer async-safe).

I was more concerned about the logging code that's called in case of an error.

* Allow a single call to virFileOpenAs() to first attempt the open as
   the current user, and if that fails to automatically re-try after
   doing fork+setuid (if deemed appropriate, i.e. errno indicates it
   would now be successful, and the file is on a networkFS). This makes
   it possible (in many, but possibly not all, cases) to drop-in
   virFileOpenAs() as a replacement for open(2).
Sounds like a nice cleanup, especially since we previously had several
call sites that had to dance around virFileOpenAs twice to get the same
behavior.  Refactoring the double attempt into the common code is good.

I did it more for new uses of virFileOpenAs in the future, rather than the existing dual use (which is used by several consumers, but consolidated into qemuOpenFile(). The problem is that it uses a different mode depending on whether it opens the file as root, or as the qemu user, and although that usage doesn't force the file permissions when opening an existing file, it would end up producing different results when creating a new file (with the existing code, a file created w/o forking would be mode 600 and one requiring fork+setuid would be created with mod 660). To reduce it to a single call, we would need to choose either 600 or 660 for both cases - for security reasons, we wouldn't want to choose 660; 600 would be okay as long as nobody was trying to access the newly created file outside of libvirt or qemu, and trying to do so from a process running with group qemu, but some different user - that's the thing that I mentioned needs at least some discussion before changing behavior.

   (NB: currently qemuOpenFile() calls virFileOpenAs() twice, once
   without forking, then again with forking. That unfortunately can't
   be changed without at least some discussion of the ramifications,
   because the requested file permissions are different in each case,
   which is something that a single call to virFileOpenAs() can't deal
For a pre-existing file, all that matters is that you get an fd open; it
doesn't matter who opened it.  (Unless we really _do_ want to change
ownerships as part of opening a file.)

For creating a new file, it matters that the resulting fd is owned by
the correct user.  If root creates the file, but the file will be passed
to qemu, then we must change ownership (fchmod or grant an ACL); or we
must open the file as qemu in the first place.  The former will be
prevented by root-squash NFS, so NFS already has the right behavior;

That is all true, as long as nobody expects to access the file outside libvirt+qemu.

anywhere else, root is powerful enough to be able to fchown the file
over to the right user.

I guess I'll see more on this as I continue reviewing.

* Add a flag so that any fchown() of the file to a different uid:gid
   is explicitly requested when the function is called, rather than it
   being implied by the presence of the O_CREAT flag. This just makes
   for less subtle surprises to consumers.
Yes, I think that makes sense.  We really are doing two things at once:
'give me an fd', and 'ensure that the right people down the road can
call open and get a new fd to the same file'; separating the fchown and
making it only happen when explicitly requested means that by default we
just get the fd.

Callers passing O_CREAT | O_TRUNC don't care if the file previously
existed, so they probably won't pass the ownership flag.  Callers
passing O_CREAT | O_EXCL are trying to create a new file, so they
probably care about ownership on the new file.  But making them be
explicit about it makes it easier to review.

   b1643dc15c5de886fefe56ad18608d65f1325a2c added the check for O_CREAT
   before forcing ownership. This patch just makes that restriction
   more explicit.)

* If either the uid or gid is specified as "-1", virFileOpenAs will
   interpret this to mean "the current [gu]id".

All current consumers of virFileOpenAs should retain their present
behavior (after a few minor changes to their setup code and
  src/libxl/libxl_driver.c      |    4 +-
  src/qemu/qemu_driver.c        |    8 +-
  src/storage/storage_backend.c |   12 +-
  src/util/util.c               |  341 ++++++++++++++++++++++++-----------------
  src/util/util.h               |    6 +-
  5 files changed, 217 insertions(+), 154 deletions(-)

diff --git a/src/libxl/libxl_driver.c b/src/libxl/libxl_driver.c
index 0500ed0..d7325c3 100644
--- a/src/libxl/libxl_driver.c
+++ b/src/libxl/libxl_driver.c
@@ -216,7 +216,7 @@ libxlSaveImageOpen(libxlDriverPrivatePtr driver, const char *from,
      libxlSavefileHeader hdr;
      char *xml = NULL;

-    if ((fd = virFileOpenAs(from, O_RDONLY, 0, getuid(), getgid(), 0))<  0) {
+    if ((fd = virFileOpenAs(from, O_RDONLY, 0, -1, -1, 0))<  0) {
Based solely on the commit message, this is a valid conversion (I guess
when I get to the actual function, to see if the new implementation
matches the commit, determines whether I have to change my mind :)

@@ -1827,7 +1827,7 @@ libxlDoDomainSave(libxlDriverPrivatePtr driver, virDomainObjPtr vm,

      if ((fd = virFileOpenAs(to, O_CREAT|O_TRUNC|O_WRONLY, S_IRUSR|S_IWUSR,
-                            getuid(), getgid(), 0))<  0) {
+                            -1, -1, 0))<  0) {
Here, we're creating a file, but I guess we're okay if it ends up being
owned by root and not some other user.

That's actually a NOP change - previously it sent getuid() as the user, now it sends -1, which is translated to getuid() by virFileOpenAs.

+++ b/src/qemu/qemu_driver.c
@@ -2306,6 +2306,7 @@ qemuOpenFile(struct qemud_driver *driver, const char *path, int oflags,
      bool is_reg = true;
      bool need_unlink = false;
      bool bypass_security = false;
+    unsigned int vfoflags = 0;
      int fd = -1;
      uid_t uid = getuid();
      gid_t gid = getgid();
@@ -2315,6 +2316,7 @@ qemuOpenFile(struct qemud_driver *driver, const char *path, int oflags,
       * in the failure case */
      if (oflags&  O_CREAT) {
          need_unlink = true;
+        vfoflags |= VIR_FILE_OPEN_FORCE_OWNER;
Per the commit message, this is preserving semantics of the old behavior.

          if (stat(path,&sb) == 0) {
              is_reg = !!S_ISREG(sb.st_mode);
              /* If the path is regular file which exists
@@ -2335,8 +2337,8 @@ qemuOpenFile(struct qemud_driver *driver, const char *path, int oflags,
              goto cleanup;
      } else {
-        if ((fd = virFileOpenAs(path, oflags, S_IRUSR | S_IWUSR,
-                                uid, gid, 0))<  0) {
+        if ((fd = virFileOpenAs(path, oflags, S_IRUSR | S_IWUSR, uid, gid,
+                                vfoflags | VIR_FILE_OPEN_NOFORK))<  0) {
              /* If we failed as root, and the error was permission-denied
                 (EACCES or EPERM), assume it's on a network-connected share
                 where root access is restricted (eg, root-squashed NFS). If the
@@ -2380,7 +2382,7 @@ qemuOpenFile(struct qemud_driver *driver, const char *path, int oflags,
              if ((fd = virFileOpenAs(path, oflags,
                                      driver->user, driver->group,
-                                    VIR_FILE_OPEN_AS_UID))<  0) {
+                                    vfoflags | VIR_FILE_OPEN_FORK))<  0) {
And this keeps things as two calls for now, for minimal code churn in
the clients, so that this patch focuses on the implementation (I'm
guessing 2/2 simplifies the clients).

Actually (as you'll find out) 2/2 just uses virFileOpenAs in a new place to fix a bug. Reducing the usage in qemuOpenFile() to a single call was left alone due to the issue outlined above. I'm starting to feel more brave about making that change, though :-) (Maybe now is the time to make it, giving us more testing time to see if it breaks anyone's usage).

@@ -393,15 +391,15 @@ virStorageBackendCreateRaw(virConnectPtr conn ATTRIBUTE_UNUSED,
          goto cleanup;

-    uid = (vol->target.perms.uid == -1) ? getuid() : vol->target.perms.uid;
-    gid = (vol->target.perms.gid == -1) ? getgid() : vol->target.perms.gid;
-    operation_flags = VIR_FILE_OPEN_FORCE_PERMS;
      if (pool->def->type == VIR_STORAGE_POOL_NETFS)
-        operation_flags |= VIR_FILE_OPEN_AS_UID;
+        operation_flags |= VIR_FILE_OPEN_FORK;

      if ((fd = virFileOpenAs(vol->target.path,
                              O_RDWR | O_CREAT | O_EXCL,
-                            vol->target.perms.mode, uid, gid,
+                            vol->target.perms.mode,
+                            vol->target.perms.uid,
+                            vol->target.perms.gid,
Hmm, we still have the problem that we are storing _virStoragePerms as
int mode, int uid, and int gid, instead of the better mode_t mode, uid_t
uid, and gid_t gid (and given our recent problems with 64-bit pid_t,
it's not too much of a stretch to imagine a system with 64-bit uid_t,
even though mingw64 is thankfully not such a system).  But that's a
separate cleanup, and I'll probably be working on it in parallel with my
pid_t cleanups.  So I'm okay with this change.

-/* return -errno on failure, or 0 on success */
+/* virFileOpenForked() - an internal utility function called only by
+ * virFileOpenAs(). It forks, then the child does setuid+setgid to
+ * given uid:gid and attempts to open the file, while the parent just
+ * calls recvfd to get the open fd back from the child. returns the
+ * fd, or -errno if there is an error. */
  static int
I'm going to review this more on the basis of post-patch, rather than on
the diff (as you mentioned, it is enough of a change to make the diff
difficult).  But unfortunately, I've run out of time at the moment; I'll
see if I can get back to the review later tonight, otherwise it will be
tomorrow morning.

You'll probably notice what I just noticed:

-    ret = virFileOpenAsNoFork(path, openflags, mode, uid, gid, flags);
-    if (ret<  0)
+    if ((fd = open(path, openflags, mode))<  0) {
+        ret = --errno;
         goto childerror;
-    fd = ret;
+    }
So it will return EACCES-1 instead of -EACCES :-P

+++ b/src/util/util.h
@@ -90,8 +90,10 @@ char *virFileSanitizePath(const char *path);

  enum {
      VIR_FILE_OPEN_NONE        = 0,
-    VIR_FILE_OPEN_AS_UID      = (1<<  0),
-    VIR_FILE_OPEN_FORCE_PERMS = (1<<  1),
+    VIR_FILE_OPEN_NOFORK      = (1<<  0),
+    VIR_FILE_OPEN_FORK        = (1<<  1),
+    VIR_FILE_OPEN_FORCE_MODE  = (1<<  2),
+    VIR_FILE_OPEN_FORCE_OWNER = (1<<  3),
The new flag names make sense.  It looked like in this round, you used
NOFORK and FORK as mutually exclusive flags; I'm guessing that the next
patch mixes the two as the way to state that you want to use a
double-open attempt,

Correct. And if you specify neither, it equates that to having set them both, just to make the most common use case simpler to write.

   And to some degree, FORCE_PERMS maps to
FORCE_MODE, and AS_UID maps to FORCE_OWNER, but with clearer semantics -

AS_UID == FORK, FORCE_OWNER is a separate issue that was previously implied by setting O_CREAT.

if the flag is present, guarantee an fchmod() or fchown(), whether or
not a file was created; if the flag is not present, focus only on
getting the fd.


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]