changeset 10308:a0c54044a2bd

6839110 ls hangs when run on smbfs mounted directory containing many files 6699861 Directory access denied is not reported correctly
author Gordon Ross <Gordon.Ross@Sun.COM>
date Fri, 14 Aug 2009 12:16:18 -0400
parents 05a4e6d289cc
children 1b8c848f3840
files usr/src/uts/common/fs/smbclnt/smbfs/smbfs_node.h usr/src/uts/common/fs/smbclnt/smbfs/smbfs_smb.c usr/src/uts/common/fs/smbclnt/smbfs/smbfs_subr.h usr/src/uts/common/fs/smbclnt/smbfs/smbfs_subr2.c usr/src/uts/common/fs/smbclnt/smbfs/smbfs_vnops.c
diffstat 5 files changed, 253 insertions(+), 230 deletions(-) [+]
line wrap: on
line diff
--- a/usr/src/uts/common/fs/smbclnt/smbfs/smbfs_node.h	Fri Aug 14 02:34:40 2009 -0700
+++ b/usr/src/uts/common/fs/smbclnt/smbfs/smbfs_node.h	Fri Aug 14 12:16:18 2009 -0400
@@ -217,12 +217,11 @@
 	uint32_t	n_flag;
 	smbmntinfo_t	*n_mount;
 	ino64_t		n_ino;
-	/* Lock for the next 8 is r_lkserlock */
+	/* Lock for the next 7 is r_lkserlock */
 	enum vtype	n_ovtype;	/* vnode type opened */
 	int		n_dirrefs;
 	struct smbfs_fctx	*n_dirseq;	/* ff context */
-	long		n_dirofs;	/* last ff offset */
-	long		n_direof;	/* End of dir. offset. */
+	int		n_dirofs;	/* last ff offset */
 	int		n_vcgenid;	/* gereration no. (reconnect) */
 	int		n_fidrefs;
 	uint16_t	n_fid;		/* file handle */
--- a/usr/src/uts/common/fs/smbclnt/smbfs/smbfs_smb.c	Fri Aug 14 02:34:40 2009 -0700
+++ b/usr/src/uts/common/fs/smbclnt/smbfs/smbfs_smb.c	Fri Aug 14 12:16:18 2009 -0400
@@ -560,18 +560,6 @@
 		md_get_mem(mdp, fsa->fsa_tname, nlen, MB_MSYSTEM);
 	}
 
-	/*
-	 * If fs_name isn't NTFS they probably require resume keys.
-	 * This is another example of the client trying to fix a server
-	 * bug. This code uses the logic created by PR-3983209. See
-	 * long block comment in smbfs_smb_findnextLM2.
-	 */
-	if (strcmp(fsa->fsa_tname, "NTFS")) {
-		SMB_SS_LOCK(ssp);
-		ssp->ss_flags |= SMBS_RESUMEKEYS;
-		SMB_SS_UNLOCK(ssp);
-	}
-
 	smb_t2_done(t2p);
 	return (0);
 }
@@ -2304,14 +2292,13 @@
 	struct smb_vc *vcp = SSTOVC(ctx->f_ssp);
 	struct mbchain *mbp;
 	struct mdchain *mdp;
-	uint16_t tw, flags;
+	uint16_t ecnt, eos, lno, flags;
 	int len, error;
 
 	if (ctx->f_t2) {
 		smb_t2_done(ctx->f_t2);
 		ctx->f_t2 = NULL;
 	}
-	ctx->f_flags &= ~SMBFS_RDD_GOTRNAME;
 	flags = FIND2_RETURN_RESUME_KEYS | FIND2_CLOSE_ON_EOS;
 	if (ctx->f_flags & SMBFS_RDD_FINDSINGLE) {
 		flags |= FIND2_CLOSE_AFTER_REQUEST;
@@ -2330,7 +2317,6 @@
 		mb_put_uint16le(mbp, flags);
 		mb_put_uint16le(mbp, ctx->f_infolevel);
 		mb_put_uint32le(mbp, 0);
-		/* mb_put_uint8(mbp, SMB_DT_ASCII); specs? hah! */
 		len = ctx->f_wclen;
 		error = smbfs_fullpath(mbp, vcp, ctx->f_dnp, ctx->f_wildcard,
 		    &len, '\\');
@@ -2347,11 +2333,10 @@
 		mb_put_uint16le(mbp, ctx->f_Sid);
 		mb_put_uint16le(mbp, ctx->f_limit);
 		mb_put_uint16le(mbp, ctx->f_infolevel);
-		if (ctx->f_ssp->ss_flags & SMBS_RESUMEKEYS) {
-			mb_put_uint32le(mbp, ctx->f_rkey);
-		} else
-			mb_put_uint32le(mbp, 0);
+		/* Send whatever resume key we received... */
+		mb_put_uint32le(mbp, ctx->f_rkey);
 		mb_put_uint16le(mbp, flags);
+		/* ... and the resume name if we have one. */
 		if (ctx->f_rname) {
 			/* resume file name */
 			mb_put_mem(mbp, ctx->f_rname, ctx->f_rnamelen,
@@ -2361,67 +2346,66 @@
 		if (SMB_UNICODE_STRINGS(SSTOVC(ctx->f_ssp)))
 			mb_put_uint8(mbp, 0);	/* 1st byte NULL Unicode char */
 		mb_put_uint8(mbp, 0);
-#if 0
-		struct timespec ts;
-		ts.tv_sec = 0;
-		ts.tv_nsec = 200 * 1000 * 1000;	/* 200ms */
-		if (vcp->vc_flags & SMBC_WIN95) {
-			/*
-			 * some implementations suggests to sleep here
-			 * for 200ms, due to the bug in the Win95.
-			 * I've didn't notice any problem, but put code
-			 * for it.
-			 */
-			msleep(&flags, 0, PVFS, "fix95", &ts);
-		}
-#endif
 	}
 	t2p->t2_maxpcount = 5 * 2;
-	t2p->t2_maxdcount = vcp->vc_txmax;
+	t2p->t2_maxdcount = 0xF000;	/* 64K less some overhead */
 	error = smb_t2_request(t2p);
 	if (error)
 		return (error);
+
+	/*
+	 * This is the "resume name" we just sent.
+	 * We want the new one (if any) that may be
+	 * found in the response we just received and
+	 * will now begin parsing.  Free the old one
+	 * now so we'll know if we found a new one.
+	 */
+	if (ctx->f_rname) {
+		kmem_free(ctx->f_rname, ctx->f_rnamelen);
+		ctx->f_rname = NULL;
+		ctx->f_rnamelen = 0;
+	}
+
 	mdp = &t2p->t2_rparam;
 	if (ctx->f_flags & SMBFS_RDD_FINDFIRST) {
 		if ((error = md_get_uint16le(mdp, &ctx->f_Sid)) != 0)
-			return (error);
+			goto nodata;
 		ctx->f_flags &= ~SMBFS_RDD_FINDFIRST;
 	}
-	if ((error = md_get_uint16le(mdp, &tw)) != 0)
-		return (error);
-	ctx->f_ecnt = tw; /* search count - # entries returned */
-	if ((error = md_get_uint16le(mdp, &tw)) != 0)
-		return (error);
+	md_get_uint16le(mdp, &ecnt);		/* entry count */
+	md_get_uint16le(mdp, &eos);		/* end of search */
+	md_get_uint16le(mdp, NULL);		/* EA err. off. */
+	error = md_get_uint16le(mdp, &lno);	/* last name off. */
+	if (error != 0)
+		goto nodata;
+
 	/*
-	 * tw now is the "end of search" flag. against an XP server tw
+	 * The "end of search" flag from an XP server sometimes
 	 * comes back zero when the prior find_next returned exactly
 	 * the number of entries requested.  in which case we'd try again
-	 * but the search has in fact been closed so an EBADF results.  our
-	 * circumvention is to check here for a zero search count.
+	 * but the search has in fact been closed so an EBADF results.
+	 * our circumvention is to check here for a zero entry count.
 	 */
-	if (tw || ctx->f_ecnt == 0)
+	ctx->f_ecnt = ecnt;
+	if (eos || ctx->f_ecnt == 0)
 		ctx->f_flags |= SMBFS_RDD_EOF | SMBFS_RDD_NOCLOSE;
-	if ((error = md_get_uint16le(mdp, &tw)) != 0)
-		return (error);
-	if ((error = md_get_uint16le(mdp, &tw)) != 0)
-		return (error);
 	if (ctx->f_ecnt == 0)
 		return (ENOENT);
-	ctx->f_rnameofs = tw;
-	mdp = &t2p->t2_rdata;
-	if (mdp->md_top == NULL) {
-		SMBVDEBUG("ecnt = %d, but data is NULL\n", ctx->f_ecnt);
-		return (ENOENT);
-	}
-#ifdef APPLE
-	if (mdp->md_top->m_len == 0) {
-		printf("bug: ecnt = %d, but m_len = 0 and m_next = %p "
-		    "(please report)\n", ctx->f_ecnt, mbp->mb_top->m_next);
-		return (ENOENT);
-	}
-#endif
+
+	/* Last Name Off (LNO) is the entry with the resume name. */
+	ctx->f_rnameofs = lno;
 	ctx->f_eofs = 0;
 	return (0);
+
+nodata:
+	/*
+	 * Failed parsing the FindFirst or FindNext response.
+	 * Force this directory listing closed, otherwise the
+	 * calling process may hang in an infinite loop.
+	 */
+	ctx->f_ecnt = 0; /* Force closed. */
+	ctx->f_flags |= SMBFS_RDD_EOF;
+	return (EIO);
 }
 
 static int
@@ -2478,11 +2462,9 @@
 	uint16_t date, time, wattr;
 	uint32_t size, next, dattr, resumekey = 0;
 	uint64_t llongint;
-	int error, svtz, cnt, fxsz, nmlen, recsz, otw;
+	int error, svtz, cnt, fxsz, nmlen, recsz;
 	struct timespec ts;
 
-again:
-	otw = 0;	/* nothing sent Over The Wire (yet) */
 	if (ctx->f_ecnt == 0) {
 		if (ctx->f_flags & SMBFS_RDD_EOF)
 			return (ENOENT);
@@ -2493,7 +2475,6 @@
 			return (error);
 		ctx->f_attr.fa_reqtime = ts;
 		ctx->f_otws++;
-		otw = 1;
 	}
 	t2p = ctx->f_t2;
 	mdp = &t2p->t2_rdata;
@@ -2515,7 +2496,9 @@
 		md_get_uint32le(mdp, NULL);	/* allocation size */
 		md_get_uint16le(mdp, &wattr);
 		ctx->f_attr.fa_attr = wattr;
-		md_get_uint8(mdp, &tb);
+		error = md_get_uint8(mdp, &tb);
+		if (error)
+			goto nodata;
 		size = nmlen = tb;
 		fxsz = 23;
 		recsz = next = 24 + nmlen;	/* docs misses zero byte @end */
@@ -2537,7 +2520,9 @@
 		/* freebsd bug: fa_attr endian bug */
 		md_get_uint32le(mdp, &dattr);	/* extended file attributes */
 		ctx->f_attr.fa_attr = dattr;
-		md_get_uint32le(mdp, &size);	/* name len */
+		error = md_get_uint32le(mdp, &size);	/* name len */
+		if (error)
+			goto nodata;
 		fxsz = 64; /* size ofinfo up to filename */
 		if (ctx->f_infolevel == SMB_FIND_BOTH_DIRECTORY_INFO) {
 			/*
@@ -2545,7 +2530,9 @@
 			 * a reserved byte, and ShortName(8.3 means 24 bytes,
 			 * as Leach defined it to always be Unicode)
 			 */
-			md_get_mem(mdp, NULL, 30, MB_MSYSTEM);
+			error = md_get_mem(mdp, NULL, 30, MB_MSYSTEM);
+			if (error)
+				goto nodata;
 			fxsz += 30;
 		}
 		recsz = next ? next : fxsz + size;
@@ -2565,15 +2552,16 @@
 
 	error = md_get_mem(mdp, cp, nmlen, MB_MSYSTEM);
 	if (error)
-		return (error);
+		goto nodata;
 	if (next) {
+		/* How much data to skip? */
 		cnt = next - nmlen - fxsz;
+		if (cnt < 0) {
+			SMBVDEBUG("out of sync\n");
+			goto nodata;
+		}
 		if (cnt > 0)
 			md_get_mem(mdp, NULL, cnt, MB_MSYSTEM);
-		else if (cnt < 0) {
-			SMBVDEBUG("out of sync\n");
-			return (EBADRPC);
-		}
 	}
 	/* Don't count any trailing null in the name. */
 	if (SMB_UNICODE_STRINGS(SSTOVC(ctx->f_ssp))) {
@@ -2584,63 +2572,27 @@
 			nmlen--;
 	}
 	if (nmlen == 0)
-		return (EBADRPC);
+		goto nodata;
 
 	/*
-	 * Ref radar 3983209.  On a find-next we expect a server will
-	 * 1) if the continue bit is set, use the server's idea of current loc,
-	 * 2) else if the resume key is non-zero, use that location,
-	 * 3) else if the resume name is set, use that location,
-	 * 4) else use the server's idea of current location.
-	 *
-	 * Current NetApps don't do that.  If we send no continue bit, a zero
-	 * resume key, and a resume name, the NetApp ignores the resume name
-	 * and acts on the (zero) resume key, sending back the start of the
-	 * directory again.  Panther doesn't expose the netapp bug; Panther used
-	 * the continue bit, but that was changed for 2866172. Win2000 as a
-	 * client also relies upon the resume name, but they request a very
-	 * large number of files, so the bug would be seen only with very
-	 * large directories.
-	 *
-	 * Our fix is to notice if the second OTW op (the first find-next)
-	 * returns, in the first filename, the same filename we got back
-	 * at the start of the first OTW (the find-first).  In that case
-	 * we've detected the server bug and set SMBS_RESUMEKEYS, causing us
-	 * to send non-zero resume keys henceforth.
+	 * On a find-next we expect that the server will:
+	 * 1) if the continue bit is set, use the server's offset,
+	 * 2) else if the resume key is non-zero, use that offset,
+	 * 3) else if the resume name is set, use that offset,
+	 * 4) else use the server's idea of current offset.
 	 *
-	 * Caveat: if there's a netapp so old it doesn't negotiate NTLM 0.12
-	 * then we get no resume keys so f_rkey stays zero and this "fix"
-	 * changes nothing.
-	 *
-	 * Note due to a similar problem (4051871) we also set SMBS_RESUMEKEYS
-	 * for FAT volumes, at mount time.
+	 * We always set the resume key flag. If the server returns
+	 * a resume key then we should always send it back to them.
 	 */
-	if (otw && !(ctx->f_ssp->ss_flags & SMBS_RESUMEKEYS)) {
-		if (ctx->f_otws == 1) {
-			ctx->f_firstnmlen = nmlen;
-			ctx->f_firstnm = kmem_alloc(nmlen, KM_SLEEP);
-			bcopy(ctx->f_name, ctx->f_firstnm, nmlen);
-		} else if (ctx->f_otws == 2 && nmlen == ctx->f_firstnmlen &&
-		    !(bcmp(ctx->f_name, ctx->f_firstnm, nmlen) == 0)) {
-			struct smb_share *ssp = ctx->f_ssp;
-			SMBERROR(
-			    "server resume_name bug seen; using resume keys\n");
-			SMB_SS_LOCK(ssp);
-			ssp->ss_flags |= SMBS_RESUMEKEYS;
-			SMB_SS_UNLOCK(ssp);
-			ctx->f_ecnt = 0;
-			goto again; /* must redo last otw op! */
-		}
-	}
 	ctx->f_rkey = resumekey;
 
 	next = ctx->f_eofs + recsz;
 	if (ctx->f_rnameofs &&
-	    (ctx->f_flags & SMBFS_RDD_GOTRNAME) == 0 &&
-	    (ctx->f_rnameofs >= ctx->f_eofs &&
-	    ctx->f_rnameofs < (int)next)) {
+	    ctx->f_rnameofs >= ctx->f_eofs &&
+	    ctx->f_rnameofs < (int)next) {
 		/*
-		 * Server needs a resume filename.
+		 * This entry is the "resume name".
+		 * Save it for the next request.
 		 */
 		if (ctx->f_rnamelen != nmlen) {
 			if (ctx->f_rname)
@@ -2649,7 +2601,6 @@
 			ctx->f_rnamelen = nmlen;
 		}
 		bcopy(ctx->f_name, ctx->f_rname, nmlen);
-		ctx->f_flags |= SMBFS_RDD_GOTRNAME;
 	}
 	ctx->f_nmlen = nmlen;
 	ctx->f_eofs = next;
@@ -2658,6 +2609,18 @@
 
 	smbfs_fname_tolocal(ctx);
 	return (0);
+
+nodata:
+	/*
+	 * Something bad has happened and we ran out of data
+	 * before we could parse all f_ecnt entries expected.
+	 * Force this directory listing closed, otherwise the
+	 * calling process may hang in an infinite loop.
+	 */
+	SMBVDEBUG("ran out of data\n");
+	ctx->f_ecnt = 0; /* Force closed. */
+	ctx->f_flags |= SMBFS_RDD_EOF;
+	return (EIO);
 }
 
 static int
@@ -2715,22 +2678,9 @@
 
 	/*
 	 * Note: "limit" (maxcount) needs to fit in a short!
-	 *
-	 * smb_lookup always uses 1, which is OK (no wildcards).
-	 * Otherwise, this is smbfs_readdir, and we want to force
-	 * limit to be in the range 3 to 1000.  The low limit (3)
-	 * is so we can always give the caller one "real" entry
-	 * (something other than "." or "..") The high limit is
-	 * just tuning. WinNT used 512, Win2k 1366.  We use 1000.
-	 *
-	 * XXX: Move the [skip . ..] gunk to our caller (readdir).
 	 */
-	if ((ctx->f_flags & SMBFS_RDD_FINDSINGLE) == 0) {
-		if (limit < 3)
-			limit = 3;
-		if (limit > 1000)
-			limit = 1000;
-	}
+	if (limit > 0xffff)
+		limit = 0xffff;
 
 	ctx->f_scred = scrp;
 	for (;;) {
--- a/usr/src/uts/common/fs/smbclnt/smbfs/smbfs_subr.h	Fri Aug 14 02:34:40 2009 -0700
+++ b/usr/src/uts/common/fs/smbclnt/smbfs/smbfs_subr.h	Fri Aug 14 12:16:18 2009 -0400
@@ -93,7 +93,6 @@
 #define	SMBFS_RDD_FINDSINGLE	0x04
 /* note	SMBFS_RDD_USESEARCH	0x08 replaced by smbfs_fctx_type */
 #define	SMBFS_RDD_NOCLOSE	0x10
-#define	SMBFS_RDD_GOTRNAME	0x1000
 
 /*
  * Search context supplied by server
--- a/usr/src/uts/common/fs/smbclnt/smbfs/smbfs_subr2.c	Fri Aug 14 02:34:40 2009 -0700
+++ b/usr/src/uts/common/fs/smbclnt/smbfs/smbfs_subr2.c	Fri Aug 14 12:16:18 2009 -0400
@@ -392,7 +392,6 @@
 	np->r_vnode = vp;
 	np->n_mount = mi;
 	np->r_hashq = rhtp;
-	np->n_direof = -1;
 	np->n_fid = SMB_FID_UNUSED;
 	np->n_uid = UID_NOBODY;
 	np->n_gid = GID_NOBODY;
--- a/usr/src/uts/common/fs/smbclnt/smbfs/smbfs_vnops.c	Fri Aug 14 02:34:40 2009 -0700
+++ b/usr/src/uts/common/fs/smbclnt/smbfs/smbfs_vnops.c	Fri Aug 14 12:16:18 2009 -0400
@@ -66,6 +66,17 @@
 #include <fs/fs_subr.h>
 
 /*
+ * We assign directory offsets like the NFS client, where the
+ * offset increments by _one_ after each directory entry.
+ * Further, the entries "." and ".." are always at offsets
+ * zero and one (respectively) and the "real" entries from
+ * the server appear at offsets starting with two.  This
+ * macro is used to initialize the n_dirofs field after
+ * setting n_dirseq with a _findopen call.
+ */
+#define	FIRST_DIROFS	2
+
+/*
  * These characters are illegal in NTFS file names.
  * ref: http://support.microsoft.com/kb/147438
  *
@@ -285,9 +296,18 @@
 	}
 
 	/*
-	 * Directory open is easy.
+	 * Directory open.  See smbfs_readvdir()
 	 */
 	if (vp->v_type == VDIR) {
+		if (np->n_dirseq == NULL) {
+			/* first open */
+			error = smbfs_smb_findopen(np, "*", 1,
+			    SMB_FA_SYSTEM | SMB_FA_HIDDEN | SMB_FA_DIR,
+			    &scred, &np->n_dirseq);
+			if (error != 0)
+				goto out;
+		}
+		np->n_dirofs = FIRST_DIROFS;
 		np->n_dirrefs++;
 		goto have_fid;
 	}
@@ -307,18 +327,16 @@
 	    np->n_vcgenid == ssp->ss_vcgenid) {
 		int upgrade = 0;
 
-		/* BEGIN CSTYLED */
 		if ((flag & FWRITE) &&
 		    !(np->n_rights & (SA_RIGHT_FILE_WRITE_DATA |
-				GENERIC_RIGHT_ALL_ACCESS |
-				GENERIC_RIGHT_WRITE_ACCESS)))
+		    GENERIC_RIGHT_ALL_ACCESS |
+		    GENERIC_RIGHT_WRITE_ACCESS)))
 			upgrade = 1;
 		if ((flag & FREAD) &&
 		    !(np->n_rights & (SA_RIGHT_FILE_READ_DATA |
-				GENERIC_RIGHT_ALL_ACCESS |
-				GENERIC_RIGHT_READ_ACCESS)))
+		    GENERIC_RIGHT_ALL_ACCESS |
+		    GENERIC_RIGHT_READ_ACCESS)))
 			upgrade = 1;
-		/* END CSTYLED */
 		if (!upgrade) {
 			/*
 			 *  the existing open is good enough
@@ -509,6 +527,7 @@
 			goto out;
 		if ((fctx = np->n_dirseq) != NULL) {
 			np->n_dirseq = NULL;
+			np->n_dirofs = 0;
 			error = smbfs_smb_findclose(fctx, &scred);
 		}
 	} else {
@@ -2573,49 +2592,69 @@
 smbfs_readvdir(vnode_t *vp, uio_t *uio, cred_t *cr, int *eofp,
 	caller_context_t *ct)
 {
-	size_t		dbufsiz;
-	struct dirent64 *dp;
+	/*
+	 * Note: "limit" tells the SMB-level FindFirst/FindNext
+	 * functions how many directory entries to request in
+	 * each OtW call.  It needs to be large enough so that
+	 * we don't make lots of tiny OtW requests, but there's
+	 * no point making it larger than the maximum number of
+	 * OtW entries that would fit in a maximum sized trans2
+	 * response (64k / 48).  Beyond that, it's just tuning.
+	 * WinNT used 512, Win2k used 1366.  We use 1000.
+	 */
+	static const int limit = 1000;
+	/* Largest possible dirent size. */
+	static const size_t dbufsiz = DIRENT64_RECLEN(SMB_MAXFNAMELEN);
 	struct smb_cred scred;
 	vnode_t		*newvp;
 	struct smbnode	*np = VTOSMB(vp);
-	int		nmlen, reclen, error = 0;
-	long		offset, limit;
 	struct smbfs_fctx *ctx;
+	struct dirent64 *dp;
+	ssize_t		save_resid;
+	offset_t	save_offset; /* 64 bits */
+	int		offset; /* yes, 32 bits */
+	int		nmlen, error;
+	ushort_t	reclen;
 
 	ASSERT(curproc->p_zone == VTOSMI(vp)->smi_zone);
 
 	/* Make sure we serialize for n_dirseq use. */
 	ASSERT(smbfs_rw_lock_held(&np->r_lkserlock, RW_WRITER));
 
-	/* Min size is DIRENT64_RECLEN(256) rounded up. */
-	if (uio->uio_resid < 512 || uio->uio_offset < 0)
+	/*
+	 * Make sure smbfs_open filled in n_dirseq
+	 */
+	if (np->n_dirseq == NULL)
+		return (EBADF);
+
+	/* Check for overflow of (32-bit) directory offset. */
+	if (uio->uio_loffset < 0 || uio->uio_loffset > INT32_MAX ||
+	    (uio->uio_loffset + uio->uio_resid) > INT32_MAX)
 		return (EINVAL);
 
+	/* Require space for at least one dirent. */
+	if (uio->uio_resid < dbufsiz)
+		return (EINVAL);
+
+#ifdef USE_DNLC
 	/*
 	 * This dnlc_purge_vp ensures that name cache for this dir will be
 	 * current - it'll only have the items for which the smbfs_nget
 	 * MAKEENTRY happened.
 	 */
-#ifdef NOT_YET
 	if (smbfs_fastlookup)
 		dnlc_purge_vp(vp);
 #endif
 	SMBVDEBUG("dirname='%s'\n", np->n_rpath);
 	smb_credinit(&scred, cr);
-	dbufsiz = DIRENT64_RECLEN(SMB_MAXFNAMELEN);
 	dp = kmem_alloc(dbufsiz, KM_SLEEP);
 
-	offset = uio->uio_offset; /* NB: "cookie" */
-	limit = uio->uio_resid / DIRENT64_RECLEN(1);
-	SMBVDEBUG("offset=0x%ld, limit=0x%ld\n", offset, limit);
-
-	if (offset == 0) {
-		/* Don't know EOF until findclose */
-		np->n_direof = -1;
-	} else if (offset == np->n_direof) {
-		/* Arrived at end of directory. */
-		goto out;
-	}
+	save_resid = uio->uio_resid;
+	save_offset = uio->uio_loffset;
+	offset = uio->uio_offset;
+	SMBVDEBUG("in: offset=%d, resid=%d\n",
+	    (int)uio->uio_offset, (int)uio->uio_resid);
+	error = 0;
 
 	/*
 	 * Generate the "." and ".." entries here so we can
@@ -2623,20 +2662,24 @@
 	 * (2) deal with getting their I numbers which the
 	 * findnext below does only for normal names.
 	 */
-	while (limit && offset < 2) {
-		limit--;
+	while (offset < FIRST_DIROFS) {
+		/*
+		 * Tricky bit filling in the first two:
+		 * offset 0 is ".", offset 1 is ".."
+		 * so strlen of these is offset+1.
+		 */
 		reclen = DIRENT64_RECLEN(offset + 1);
+		if (uio->uio_resid < reclen)
+			goto out;
 		bzero(dp, reclen);
-		/*LINTED*/
 		dp->d_reclen = reclen;
-		/* Tricky: offset 0 is ".", offset 1 is ".." */
 		dp->d_name[0] = '.';
 		dp->d_name[1] = '.';
 		dp->d_name[offset + 1] = '\0';
 		/*
 		 * Want the real I-numbers for the "." and ".."
 		 * entries.  For these two names, we know that
-		 * smbfslookup can do this all locally.
+		 * smbfslookup can get the nodes efficiently.
 		 */
 		error = smbfslookup(vp, dp->d_name, &newvp, cr, 1, ct);
 		if (error) {
@@ -2645,21 +2688,29 @@
 			dp->d_ino = VTOSMB(newvp)->n_ino;
 			VN_RELE(newvp);
 		}
-		dp->d_off = offset + 1;  /* see d_off below */
-		error = uiomove(dp, dp->d_reclen, UIO_READ, uio);
+		/*
+		 * Note: d_off is the offset that a user-level program
+		 * should seek to for reading the NEXT directory entry.
+		 * See libc: readdir, telldir, seekdir
+		 */
+		dp->d_off = offset + 1;
+		error = uiomove(dp, reclen, UIO_READ, uio);
 		if (error)
 			goto out;
+		/*
+		 * Note: uiomove updates uio->uio_offset,
+		 * but we want it to be our "cookie" value,
+		 * which just counts dirents ignoring size.
+		 */
 		uio->uio_offset = ++offset;
 	}
-	if (limit == 0)
-		goto out;
-	if (offset != np->n_dirofs || np->n_dirseq == NULL) {
-		SMBVDEBUG("Reopening search %ld:%ld\n", offset, np->n_dirofs);
-		if (np->n_dirseq) {
-			(void) smbfs_smb_findclose(np->n_dirseq, &scred);
-			np->n_dirseq = NULL;
-		}
-		np->n_dirofs = 2;
+
+	/*
+	 * If there was a backward seek, we have to reopen.
+	 */
+	if (offset < np->n_dirofs) {
+		SMBVDEBUG("Reopening search %d:%d\n",
+		    offset, np->n_dirofs);
 		error = smbfs_smb_findopen(np, "*", 1,
 		    SMB_FA_SYSTEM | SMB_FA_HIDDEN | SMB_FA_DIR,
 		    &scred, &ctx);
@@ -2667,72 +2718,97 @@
 			SMBVDEBUG("can not open search, error = %d", error);
 			goto out;
 		}
+		/* free the old one */
+		(void) smbfs_smb_findclose(np->n_dirseq, &scred);
+		/* save the new one */
 		np->n_dirseq = ctx;
-	} else
+		np->n_dirofs = FIRST_DIROFS;
+	} else {
 		ctx = np->n_dirseq;
-	while (np->n_dirofs < offset) {
-		if (smbfs_smb_findnext(ctx, offset - np->n_dirofs++,
-		    &scred) != 0) {
-			(void) smbfs_smb_findclose(np->n_dirseq, &scred);
-			np->n_dirseq = NULL;
-			np->n_direof = np->n_dirofs;
-			np->n_dirofs = 0;
-			*eofp = 1;
-			error = 0;
-			goto out;
-		}
 	}
-	error = 0;
-	for (; limit; limit--) {
+
+	/*
+	 * Skip entries before the requested offset.
+	 */
+	while (np->n_dirofs < offset) {
 		error = smbfs_smb_findnext(ctx, limit, &scred);
-		if (error) {
-			if (error == EBADRPC)
-				error = ENOENT;
-			(void) smbfs_smb_findclose(np->n_dirseq, &scred);
-			np->n_dirseq = NULL;
-			np->n_direof = np->n_dirofs;
-			np->n_dirofs = 0;
-			*eofp = 1;
-			error = 0;
-			break;
-		}
+		if (error != 0)
+			goto out;
 		np->n_dirofs++;
+	}
+
+	/*
+	 * While there's room in the caller's buffer:
+	 *	get a directory entry from SMB,
+	 *	convert to a dirent, copyout.
+	 * We stop when there is no longer room for a
+	 * maximum sized dirent because we must decide
+	 * before we know anything about the next entry.
+	 */
+	while (uio->uio_resid >= dbufsiz) {
+		error = smbfs_smb_findnext(ctx, limit, &scred);
+		if (error != 0)
+			goto out;
+		np->n_dirofs++;
+
 		/* Sanity check the name length. */
 		nmlen = ctx->f_nmlen;
 		if (nmlen > SMB_MAXFNAMELEN) {
 			nmlen = SMB_MAXFNAMELEN;
 			SMBVDEBUG("Truncating name: %s\n", ctx->f_name);
 		}
-		reclen = DIRENT64_RECLEN(nmlen);
-		if (uio->uio_resid < reclen)
-			break;
-		bzero(dp, reclen);
-		/*LINTED*/
-		dp->d_reclen = reclen;
-		dp->d_ino = ctx->f_attr.fa_ino;
-		/*
-		 * Note: d_off is the offset that a user-level program
-		 * should seek to for reading the _next_ directory entry.
-		 * See libc: readdir, telldir, seekdir
-		 */
-		dp->d_off = offset + 1;
-		bcopy(ctx->f_name, dp->d_name, nmlen);
-		dp->d_name[nmlen] = '\0';
 #ifdef NOT_YET
 		if (smbfs_fastlookup) {
-			if (smbfs_nget(vp, ctx->f_name,
-			    ctx->f_nmlen, &ctx->f_attr, &newvp) == 0)
+			if (smbfs_nget(vp, ctx->f_name, nmlen,
+			    &ctx->f_attr, &newvp) == 0)
 				VN_RELE(newvp);
 		}
 #endif /* NOT_YET */
-		error = uiomove(dp, dp->d_reclen, UIO_READ, uio);
+
+		reclen = DIRENT64_RECLEN(nmlen);
+		bzero(dp, reclen);
+		dp->d_reclen = reclen;
+		bcopy(ctx->f_name, dp->d_name, nmlen);
+		dp->d_name[nmlen] = '\0';
+		dp->d_ino = ctx->f_attr.fa_ino;
+		dp->d_off = offset + 1;	/* See d_off comment above */
+		error = uiomove(dp, reclen, UIO_READ, uio);
 		if (error)
-			break;
+			goto out;
+		/* See comment re. uio_offset above. */
 		uio->uio_offset = ++offset;
 	}
-	if (error == ENOENT)
+
+out:
+	/*
+	 * When we come to the end of a directory, the
+	 * SMB-level functions return ENOENT, but the
+	 * caller is not expecting an error return.
+	 *
+	 * Also note that we must delay the call to
+	 * smbfs_smb_findclose(np->n_dirseq, ...)
+	 * until smbfs_close so that all reads at the
+	 * end of the directory will return no data.
+	 */
+	if (error == ENOENT) {
 		error = 0;
-out:
+		if (eofp)
+			*eofp = 1;
+	}
+	/*
+	 * If we encountered an error (i.e. "access denied")
+	 * from the FindFirst call, we will have copied out
+	 * the "." and ".." entries leaving offset == 2.
+	 * In that case, restore the original offset/resid
+	 * so the caller gets no data with the error.
+	 */
+	if (error != 0 && offset == FIRST_DIROFS) {
+		uio->uio_loffset = save_offset;
+		uio->uio_resid = save_resid;
+	}
+	SMBVDEBUG("out: offset=%d, resid=%d\n",
+	    (int)uio->uio_offset, (int)uio->uio_resid);
+
 	kmem_free(dp, dbufsiz);
 	smb_credrele(&scred);
 	return (error);