Skip site navigation (1)Skip section navigation (2)
Date:      Wed, 18 Jan 2012 17:07:21 -0500
From:      John Baldwin <jhb@freebsd.org>
To:        Rick Macklem <rmacklem@freebsd.org>
Cc:        Peter Wemm <peter@freebsd.org>, fs@freebsd.org
Subject:   Race in NFS lookup can result in stale namecache entries
Message-ID:  <201201181707.21293.jhb@freebsd.org>

next in thread | raw e-mail | index | archive | help
I recently encountered a problem at work with a very stale name cache
entry.  A directory was renamed on one NFS client and a new directory
was created with the same name.  On another NFS client, both the old
and new pathnames resolved to the old filehandle and stayed that way
for days.  It was only fixed by touching the parent directory which
forced the "wrong" NFS client to flush name cache entries for the
directory and repopulate it via LOOKUPs.  I eventually figured out the
race condition that triggered this and was able to reproduce it.  (I
had to hack up the NFS client to do some explicit sleeps to order the
steps right to trigger the race however.  It seems to be very rare in
practice.)  The root cause for the stale entry being trusted is that
each per-vnode nfsnode structure has a single 'n_ctime' timestamp used
to validate positive name cache entries.  However, if there are
multiple entries for a single vnode, they were all sharing a single
timestamp.  Assume you have three threads spread across two NFS
clients (R1 on the client doing the directory rename, and T1 and T2 on
the "victim" NFS client), and assume that thread S1 represents the NFS
server and the order it completes requests.  Also, assume that $D
represents a parent directory where the rename occurs and that the
original directory is named "foo".  Finally, assume that F1 is the
original directory's filehandle, and F2 is the new filehandle.
Time increases as the graph goes down:

R1              T1              T2              S1
-------------   -------------   -------------   ---------------
                LOOKUP "$D/foo"
                  (1)
                                                REPLY (1) "foo" F1
                start reply
                 processing
                 up to
                 extracting
                 post-op attrs
RENAME "$D/foo"
  "$D/baz" (2)
                                                REPLY (2)
                                GETATTR $D
                                 during lookup
                                 due to expiry
                                 (3)
                                                REPLY (3)
                                flush $D name
                                 cache entries
                                 due to updated
                                 timestamp
                                LOOKUP "$D/baz"
                                 (4)
                                                REPLY (4) "baz" F1
                                process reply,
                                  including
                                  post-op attrs
                                  that set F1's
                                  cached attrs
                                  to a ctime
                                  post RENAME

                resume reply    finish reply
                 processing       processing
                 including        including
                 setting F1's     setting F1's
                 n_ctime and      n_ctime and
                 adding cache     adding cache
                 entry            entry

At the end of this, the "victim" NFS client now has two name cache
entries for "$D/foo" and "$D/baz" that point to the F1 filehandle.
The n_ctime used to validate these name cache hits in nfs_lookup() is
already updated to post RENAME, so nfs_lookup() will trust these
entries until a future change to F1's i-node.  Further, "$D"'s local
attribute cache already reflects the updated ctime post RENAME, so it
will not flush it's name cache entries until a future change to the
directory.

The root problem is that the name cache entry for "foo" was added
using the wrong ctime.  It really should be using the F1 attributes in
the post-op attributes from the LOOKUP reply, not from F1's local
attribute cache.  However, just changing that is not sufficient.
There are still races with the calls to cache_enter() and updating
n_ctime.

What I concluded is that it would really be far simpler and more
obvious if the cached timestamps were stored in the namecache entry
directly rather than having multiple name cache entries validated by
shared state in the nfsnode.  This does mean allowing the name cache
to hold some filesystem-specific state.  However, I felt this was much
cleaner than adding a lot more complexity to nfs_lookup().  Also, this
turns out to be fairly non-invasive to implement since nfs_lookup()
calls cache_lookup() directly, but other filesystems only call it
indirectly via vfs_cache_lookup().  I considered letting filesystems
store a void * cookie in the name cache entry and having them provide
a destructor, etc.  However, that would require extra allocations for
NFS lookups.  Instead, I just adjusted the name cache API to
explicitly allow the filesystem to store a single timestamp in a name
cache entry by adding a new 'cache_enter_time()' that accepts a struct
timespec that is copied into the entry.  'cache_enter_time()' also
saves the current value of 'ticks' in the entry.  'cache_lookup()' is
modified to add two new arguments used to return the timespec and
ticks value used for a namecache entry when a hit in the cache occurs.

One wrinkle with this is that the name cache does not create actual
entries for ".", and thus it would not store any timestamps for those
lookups.  To fix this I changed the NFS client to explicitly fast-path
lookups of "." by always returning the current directory as setup by
cache_lookup() and never bothering to do a LOOKUP or check for stale
attributes in that case.

The current patch against 8 is at
http://www.FreeBSD.org/~jhb/patches/nfs_lookup.patch

It includes ABI and API compat shims so that it is suitable for
merging to stable branches.  For HEAD I would likely retire the
cache_lookup_times() name and just change all the callers of
cache_lookup() (there are only a handful, and nwfs and smbfs might
benefit from this functionality anyway).

-- 
John Baldwin



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201201181707.21293.jhb>