changeset 494:e082e44c7fce

6244328 hsfs not resistant against malformed ISO9660 filesystems, crashes/hangs
author frankho
date Wed, 07 Sep 2005 05:57:46 -0700
parents d2df5159c731
children 310ccf2a1604
files usr/src/uts/common/fs/hsfs/hsfs_node.c usr/src/uts/common/fs/hsfs/hsfs_rrip.c usr/src/uts/common/fs/hsfs/hsfs_susp_subr.c usr/src/uts/common/fs/hsfs/hsfs_vfsops.c usr/src/uts/common/fs/hsfs/hsfs_vnops.c
diffstat 5 files changed, 162 insertions(+), 139 deletions(-) [+]
line wrap: on
line diff
--- a/usr/src/uts/common/fs/hsfs/hsfs_node.c	Wed Sep 07 04:40:39 2005 -0700
+++ b/usr/src/uts/common/fs/hsfs/hsfs_node.c	Wed Sep 07 05:57:46 2005 -0700
@@ -20,7 +20,7 @@
  * CDDL HEADER END
  */
 /*
- * Copyright 2004 Sun Microsystems, Inc.  All rights reserved.
+ * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
  * Use is subject to license terms.
  */
 
@@ -45,6 +45,7 @@
 #include <sys/fbuf.h>
 #include <sys/kmem.h>
 #include <sys/policy.h>
+#include <sys/sunddi.h>
 #include <vm/hat.h>
 #include <vm/as.h>
 #include <vm/pvn.h>
@@ -744,7 +745,7 @@
 	 * fold it to upper case.
 	 */
 	if (is_rrip) {
-		(void) strcpy(cmpname, name);
+		(void) strlcpy(cmpname, name, cmpname_size);
 		cmpnamelen = namlen;
 	} else {
 		/*
@@ -776,25 +777,18 @@
 tryagain:
 
 	while (offset < end) {
-
-		if ((offset & MAXBMASK) + MAXBSIZE > dirsiz)
-			bytes_wanted = dirsiz - (offset & MAXBMASK);
-		else
-			bytes_wanted = MAXBSIZE;
+		bytes_wanted = MIN(MAXBSIZE, dirsiz - (offset & MAXBMASK));
 
 		error = fbread(dvp, (offset_t)(offset & MAXBMASK),
 			(unsigned int)bytes_wanted, S_READ, &fbp);
 		if (error)
 			goto done;
 
-		last_offset = (offset & MAXBMASK) + fbp->fb_count - 1;
-
-#define	rel_offset(offset) ((offset) & MAXBOFFSET)  /* index into cur blk */
+		last_offset = (offset & MAXBMASK) + fbp->fb_count;
 
-		switch (process_dirblock(fbp, &offset,
-					last_offset, cmpname,
-					cmpnamelen, fsp, dhp, dvp,
-					vpp, &error, is_rrip)) {
+		switch (process_dirblock(fbp, &offset, last_offset,
+		    cmpname, cmpnamelen, fsp, dhp, dvp, vpp, &error,
+		    is_rrip)) {
 		case FOUND_ENTRY:
 			/* found an entry, either correct or not */
 			goto done;
@@ -896,7 +890,7 @@
 			hdp->nlink = 2;
 		} else {
 			hs_log_bogus_disk_warning(fsp,
-				HSFS_ERR_UNSUP_TYPE, flags);
+			    HSFS_ERR_UNSUP_TYPE, flags);
 			return (EINVAL);
 		}
 		hdp->uid = fsp -> hsfs_vol.vol_uid;
@@ -923,7 +917,7 @@
 			hdp->nlink = 2;
 		} else {
 			hs_log_bogus_disk_warning(fsp,
-				HSFS_ERR_UNSUP_TYPE, flags);
+			    HSFS_ERR_UNSUP_TYPE, flags);
 			return (EINVAL);
 		}
 		hdp->uid = fsp -> hsfs_vol.vol_uid;
@@ -987,14 +981,18 @@
 	if (NAME_HAS_CHANGED(name_change_flag))
 		return (0);
 
-	/* return the pointer to the directory name and its length */
+	/*
+	 * Fall back to the ISO name. Note that as in process_dirblock,
+	 * the on-disk filename length must be validated against ISO
+	 * limits - which, in case of RR present but no RR name found,
+	 * are NOT identical to fsp->hsfs_namemax on this filesystem.
+	 */
 	on_disk_name = (char *)HDE_name(dirp);
 	on_disk_namelen = (int)HDE_NAME_LEN(dirp);
 
-	if (((int)(fsp->hsfs_namemax) > 0) &&
-			(on_disk_namelen > (int)(fsp->hsfs_namemax))) {
+	if (on_disk_namelen > ISO_FILE_NAMELEN) {
 		hs_log_bogus_disk_warning(fsp, HSFS_ERR_BAD_FILE_LEN, 0);
-		on_disk_namelen = fsp->hsfs_namemax;
+		on_disk_namelen = ISO_FILE_NAMELEN;
 	}
 	if (dnp != NULL) {
 		namelen = hs_namecopy(on_disk_name, dnp, on_disk_namelen,
@@ -1180,15 +1178,17 @@
 	int		res;
 	int		parsedir_res;
 	size_t		rrip_name_size;
-	int		rr_namelen;
+	int		rr_namelen = 0;
 	char		*rrip_name_str = NULL;
 	char		*rrip_tmp_name = NULL;
 	enum dirblock_result err = 0;
 	int 		did_fbrelse = 0;
 	char		uppercase_name[ISO_FILE_NAMELEN];
 
-	/* return after performing cleanup-on-exit */
-#define	PD_return(retval)	{ err = retval; goto do_ret; }
+#define	PD_return(retval)	\
+	{ err = retval; goto do_ret; }		/* return after cleanup */
+#define	rel_offset(offset)	\
+	((offset) & MAXBOFFSET)			/* index into cur blk */
 
 	if (is_rrip) {
 		rrip_name_size = RRIP_FILE_NAMELEN + 1;
@@ -1202,39 +1202,61 @@
 
 		/*
 		 * Directory Entries cannot span sectors.
-		 * Unused bytes at the end of each sector are zeroed.
-		 * Therefore, detect this condition when the size
-		 * field of the directory entry is zero.
+		 *
+		 * Unused bytes at the end of each sector are zeroed
+		 * according to ISO9660, but we cannot rely on this
+		 * since both media failures and maliciously corrupted
+		 * media may return arbitrary values.
+		 * We therefore have to check for consistency:
+		 * The size of a directory entry must be at least
+		 * 34 bytes (the size of the directory entry metadata),
+		 * or zero (indicating the end-of-sector condition).
+		 * For a non-zero directory entry size of less than
+		 * 34 Bytes, log a warning.
+		 * In any case, skip the rest of this sector and
+		 * continue with the next.
 		 */
 		hdlen = (int)((uchar_t)
-				HDE_DIR_LEN(&blkp[rel_offset(*offset)]));
-		if (hdlen == 0) {
-			/* advance to next sector boundary */
-			*offset = (*offset & MAXHSMASK) + HS_SECTOR_SIZE;
+		    HDE_DIR_LEN(&blkp[rel_offset(*offset)]));
 
-			if (*offset > last_offset) {
-				/* end of block */
-				PD_return(HIT_END)
-			} else
-				continue;
+		if (hdlen < HDE_ROOT_DIR_REC_SIZE ||
+		    *offset + hdlen > last_offset) {
+			/*
+			 * Advance to the next sector boundary
+			 */
+			*offset = roundup(*offset + 1, HS_SECTOR_SIZE);
+			if (hdlen)
+				hs_log_bogus_disk_warning(fsp,
+				    HSFS_ERR_TRAILING_JUNK, 0);
+			continue;
 		}
 
-		/*
-		 * Zero out the hd to read new directory
-		 */
 		bzero(&hd, sizeof (hd));
 
 		/*
-		 * Just ignore invalid directory entries.
-		 * XXX - maybe hs_parsedir() will detect EXISTENCE bit
+		 * Check the filename length in the ISO record for
+		 * plausibility and reset it to a safe value, in case
+		 * the name length byte is out of range. Since the ISO
+		 * name will be used as fallback if the rockridge name
+		 * is invalid/nonexistant, we must make sure not to
+		 * blow the bounds and initialize dnamelen to a sensible
+		 * value within the limits of ISO9660.
+		 * In addition to that, the ISO filename is part of the
+		 * directory entry. If the filename length is too large
+		 * to fit, the record is invalid and we'll advance to
+		 * the next.
 		 */
 		dirp = &blkp[rel_offset(*offset)];
 		dname = (char *)HDE_name(dirp);
 		dnamelen = (int)((uchar_t)HDE_NAME_LEN(dirp));
-		if (dnamelen > (int)(fsp->hsfs_namemax)) {
+		if (dnamelen > hdlen - HDE_FDESIZE) {
 			hs_log_bogus_disk_warning(fsp,
-				HSFS_ERR_BAD_FILE_LEN, 0);
-			dnamelen = fsp->hsfs_namemax;
+			    HSFS_ERR_BAD_FILE_LEN, 0);
+			goto skip_rec;
+		} else if (dnamelen > ISO_FILE_NAMELEN) {
+			hs_log_bogus_disk_warning(fsp,
+			    HSFS_ERR_BAD_FILE_LEN, 0);
+			dnamelen = ISO_FILE_NAMELEN;
 		}
 
 		/*
@@ -1280,15 +1302,17 @@
 			else
 				dnamelen = strip_trailing(fsp, dname, dnamelen);
 
+			ASSERT(dnamelen <= ISO_FILE_NAMELEN);
+
 			if (uppercase_cp(dname, uppercase_name, dnamelen))
 				hs_log_bogus_disk_warning(fsp,
-					HSFS_ERR_LOWER_CASE_NM, 0);
+				    HSFS_ERR_LOWER_CASE_NM, 0);
 			dname = uppercase_name;
-			if (! is_rrip &&
-				(fsp->hsfs_flags & HSFSMNT_NOTRAILDOT) &&
-					dname[ dnamelen-1 ] == '.' &&
-					CAN_TRUNCATE_DOT(dname, dnamelen))
-				dname[ --dnamelen ] = '\0';
+			if (!is_rrip &&
+			    (fsp->hsfs_flags & HSFSMNT_NOTRAILDOT) &&
+			    dname[dnamelen - 1] == '.' &&
+			    CAN_TRUNCATE_DOT(dname, dnamelen))
+				dname[--dnamelen] = '\0';
 		}
 
 		/*
@@ -1298,28 +1322,23 @@
 
 		/* if we saw a lower case name we can't do this test either */
 		if (strict_iso9660_ordering && !is_rrip &&
-			!HSFS_HAVE_LOWER_CASE(fsp) && *nm < *dname) {
+		    !HSFS_HAVE_LOWER_CASE(fsp) && *nm < *dname) {
 			RESTORE_NM(rrip_tmp_name, nm);
 			PD_return(WENT_PAST)
 		}
 
-		if (*nm != *dname || nmlen != dnamelen) {
-			/* look at next dir entry */
-			RESTORE_NM(rrip_tmp_name, nm);
-			*offset += hdlen;
-			continue;
-		}
+		if (*nm != *dname || nmlen != dnamelen)
+			goto skip_rec;
 
 		if ((res = nmcmp(dname, nm, nmlen, is_rrip)) == 0) {
 			/* name matches */
-			parsedir_res =
-				hs_parsedir(fsp, dirp, &hd, (char *)NULL,
-					    (int *)NULL);
+			parsedir_res = hs_parsedir(fsp, dirp, &hd,
+			    (char *)NULL, (int *)NULL);
 			if (!parsedir_res) {
 				uint_t lbn;	/* logical block number */
 
 				lbn = dhp->hs_dirent.ext_lbn +
-					dhp->hs_dirent.xar_len;
+				    dhp->hs_dirent.xar_len;
 				/*
 				 * Need to do an fbrelse() on the buffer,
 				 * as hs_makenode() may try to acquire
@@ -1328,8 +1347,8 @@
 				 */
 				fbrelse(fbp, S_READ);
 				did_fbrelse = 1;
-				*vpp = hs_makenode(&hd, lbn,
-						*offset, dvp->v_vfsp);
+				*vpp = hs_makenode(&hd, lbn, *offset,
+				    dvp->v_vfsp);
 				if (*vpp == NULL) {
 					*error = ENFILE;
 					RESTORE_NM(rrip_tmp_name, nm);
@@ -1355,6 +1374,7 @@
 		 * name > dir entry,
 		 * look at next one.
 		 */
+skip_rec:
 		*offset += hdlen;
 		RESTORE_NM(rrip_tmp_name, nm);
 	}
@@ -1365,7 +1385,7 @@
 		kmem_free(rrip_name_str, rrip_name_size);
 	if (rrip_tmp_name)
 		kmem_free(rrip_tmp_name, rrip_name_size);
-	if (! did_fbrelse)
+	if (!did_fbrelse)
 		fbrelse(fbp, S_READ);
 	return (err);
 #undef PD_return
--- a/usr/src/uts/common/fs/hsfs/hsfs_rrip.c	Wed Sep 07 04:40:39 2005 -0700
+++ b/usr/src/uts/common/fs/hsfs/hsfs_rrip.c	Wed Sep 07 05:57:46 2005 -0700
@@ -20,8 +20,8 @@
  * CDDL HEADER END
  */
 /*
- * Copyright (c) 1990,1997,2000,2001 by Sun Microsystems, Inc.
- * All rights reserved.
+ * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
+ * Use is subject to license terms.
  */
 
 #pragma ident	"%Z%%M%	%I%	%E% SMI"
@@ -51,6 +51,7 @@
 #include <sys/mode.h>
 #include <sys/mkdev.h>
 #include <sys/ddi.h>
+#include <sys/sunddi.h>
 
 #include <vm/page.h>
 
@@ -206,18 +207,14 @@
 	int		rrip_flags,	/* component/name flag */
 	uchar_t		*SUA_string,	/* string from SUA */
 	size_t		SUA_string_len, /* length of SUA string */
-	uchar_t		*to_string,	/* string to copy to */
-	int		*to_string_len_p, /* ptr to cur. str len */
+	uchar_t		*dst,		/* string to copy to */
+	int		*dst_lenp,	/* ptr to cur. str len */
 	ulong_t		*name_flags_p,	/* internal name flags */
-	int		max_name_len)	/* limit dest string to */
+	int		dst_size)	/* limit dest string to */
 						/* this value */
 {
-	size_t	tmp_name_len;
-
-	if (IS_NAME_BIT_SET(rrip_flags, RRIP_NAME_ROOT)) {
-		(void) strcpy((char *)to_string, "/");
-		*to_string_len_p = 1;
-	}
+	if (IS_NAME_BIT_SET(rrip_flags, RRIP_NAME_ROOT))
+		(void) strcpy((char *)dst, "/");
 
 	if (IS_NAME_BIT_SET(rrip_flags, RRIP_NAME_CURRENT)) {
 		SUA_string = (uchar_t *)".";
@@ -243,21 +240,22 @@
 	}
 
 	/*
-	 * Remember we must strncpy to the end of the curent string
-	 * name because there might be something there.  Also, don't
-	 * go past the max_name_len system boundry.
+	 * Since SUA_string isn't NULL terminated, we use strlcat()
+	 * to add the trailing NULL byte. Defensive programming, don't
+	 * ever overwrite beyond the end of the destination buffer,
+	 * and neither attempt to read beyond the end of SUA_string.
+	 * We assume the caller properly validates SUA_string_len as
+	 * we have no way of doing so.
+	 * Note for corrupted filesystems, SUA_string may contain
+	 * NULL bytes. If that happens, the destination string length
+	 * returned will be larger than the "actual" length, since
+	 * strlcat() terminates when encountering the NULL byte.
+	 * This causes no harm (apart from filenames not being reported
+	 * 'correctly', but then what is correct on a corrupted fs ?)
+	 * so we don't bother assigning strlen(dst) to *dst_lenp.
 	 */
-	tmp_name_len = strlen((char *)to_string);
-
-	SUA_string_len = (tmp_name_len + SUA_string_len) > max_name_len ?
-		(max_name_len - tmp_name_len) : (SUA_string_len);
-
-	(void) strncpy((char *)(to_string + tmp_name_len),
-		(char *)SUA_string, SUA_string_len);
-
-	/* NULL terminate string */
-	*(to_string + tmp_name_len + SUA_string_len) = '\0';
-	*(to_string_len_p) += (int)SUA_string_len;
+	*dst_lenp = MIN(dst_size, strlen((char *)dst) + SUA_string_len + 1);
+	(void) strlcat((char *)dst, (char *)SUA_string, (*dst_lenp)--);
 
 	if (IS_NAME_BIT_SET(rrip_flags, RRIP_NAME_CONTINUE))
 		SET_NAME_BIT(*(name_flags_p), RRIP_NAME_CONTINUE);
--- a/usr/src/uts/common/fs/hsfs/hsfs_susp_subr.c	Wed Sep 07 04:40:39 2005 -0700
+++ b/usr/src/uts/common/fs/hsfs/hsfs_susp_subr.c	Wed Sep 07 05:57:46 2005 -0700
@@ -22,8 +22,8 @@
 /*
  * System Use Sharing protocol subroutines for High Sierra filesystem
  *
- * Copyright (c) 1990,2000,2001 by Sun Microsystems, Inc.
- * All rights reserved.
+ * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
+ * Use is subject to license terms.
  */
 
 #pragma ident	"%Z%%M%	%I%	%E% SMI"
@@ -183,8 +183,8 @@
 /*
  * parse_signatures()
  *
- * for the signature string that is passed to this function, find the
- * correct handling function.
+ * Find the correct handling function for the signature string that is
+ * passed to this function.
  *
  * signature searching:
  *
@@ -302,18 +302,19 @@
 		} /* for extnp	(extension parsing) */
 
 		/*
-		 * Opps, did not find this signature, so we must
-		 * advance on the the next signature in the SUA and
-		 * hope to god that it is in the susp format, or we get
-		 * hosed.
+		 * Opps, did not find this signature. We must
+		 * advance on the the next signature in the SUA
+		 * and pray to persumedly omniscient, omnipresent,
+		 * almighty transcendental being(s) that the next
+		 * record is in the susp format, or we get hosed.
 		 */
 		if (SUA_rem < SUF_MIN_LEN)
 			return (END_OF_SUA);
 
-
 		SUA_rem -= SUF_LEN(sig_string);
 		sig_string += SUF_LEN(sig_string);
 
+next_signature:
 		/*
 		 * Failsafe
 		 */
@@ -321,21 +322,6 @@
 			return (END_OF_SUA);
 		}
 
-		continue;
-
-next_signature:
-
-		/*
-		 * we just want to continue in this for loop, not
-		 * the innermost one.  Unfortunately, goto is
-		 * the only way, until we get a break that
-		 * takes an argument.
-		 *
-		 * This continue statement is not really needed, but
-		 * the compiler barfs if it is not here.
-		 */
-		continue;
-
 	} /* while */
 
 	return (END_OF_SUA);
@@ -451,6 +437,17 @@
 	int		error;
 	uint_t		secno;
 
+	/*
+	 * Guard against invalid continuation area records.
+	 * Both cont_offset and cont_len must be no longer than
+	 * HS_SECTOR_SIZE. If they are, return an error.
+	 */
+	if (cont_info_p->cont_offset > HS_SECTOR_SIZE ||
+	    cont_info_p->cont_len > HS_SECTOR_SIZE) {
+		cmn_err(CE_NOTE, "get_cont_area: invalid offset/length");
+		return (1);
+	}
+
 	if (*buf_pp == (uchar_t *)NULL)
 		*buf_pp = kmem_alloc((size_t)HS_SECTOR_SIZE, KM_SLEEP);
 
--- a/usr/src/uts/common/fs/hsfs/hsfs_vfsops.c	Wed Sep 07 04:40:39 2005 -0700
+++ b/usr/src/uts/common/fs/hsfs/hsfs_vfsops.c	Wed Sep 07 05:57:46 2005 -0700
@@ -20,7 +20,7 @@
  * CDDL HEADER END
  */
 /*
- * Copyright 2004 Sun Microsystems, Inc.  All rights reserved.
+ * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
  * Use is subject to license terms.
  */
 
@@ -466,22 +466,31 @@
 	struct buf	*bp;
 	int		error;
 	int		fsid;
-	int		size = CHECKSUM_SIZE;
 
 	secno = hsvp->root_dir.ext_lbn >> hsvp->lbn_secshift;
-	bp = bread(devvp->v_rdev, secno * 4, size);
+	bp = bread(devvp->v_rdev, secno * 4, CHECKSUM_SIZE);
 	error = geterror(bp);
-	if (!error) {
+
+	/*
+	 * An error on read or a partial read means we asked
+	 * for a nonexistant/corrupted piece of the device
+	 * (including past-the-end of the media). Don't
+	 * try to use the checksumming method then.
+	 */
+	if (!error && bp->b_bcount == CHECKSUM_SIZE) {
 		int *ibuf = (int *)bp->b_un.b_addr;
-		int isize = size / sizeof (int);
 		int i;
 
 		fsid = 0;
 
-		for (i = 0; i < isize; i++)
+		for (i = 0; i < CHECKSUM_SIZE / sizeof (int); i++)
 			fsid ^= ibuf[ i ];
-	} else	/* use creation date */
+	} else {
+		/*
+		 * Fallback - use creation date
+		 */
 		fsid = hsvp->cre_date.tv_sec;
+	}
 
 	brelse(bp);
 
@@ -626,6 +635,7 @@
 		if (hs_remakenode(fsp->hsfs_vol.root_dir.ext_lbn,
 			    (uint_t)0, vfsp, &fsp->hsfs_rootvp)) {
 			error = EINVAL;
+			mutex_exit(&hs_mounttab_lock);
 			goto cleanup;
 		}
 	} else {
--- a/usr/src/uts/common/fs/hsfs/hsfs_vnops.c	Wed Sep 07 04:40:39 2005 -0700
+++ b/usr/src/uts/common/fs/hsfs/hsfs_vnops.c	Wed Sep 07 05:57:46 2005 -0700
@@ -402,10 +402,7 @@
 	nd = (struct dirent64 *)outbuf;
 
 	while (offset < dirsiz) {
-		if ((offset & MAXBMASK) + MAXBSIZE > dirsiz)
-			bytes_wanted = dirsiz - (offset & MAXBMASK);
-		else
-			bytes_wanted = MAXBSIZE;
+		bytes_wanted = MIN(MAXBSIZE, dirsiz - (offset & MAXBMASK));
 
 		error = fbread(vp, (offset_t)(offset & MAXBMASK),
 			(unsigned int)bytes_wanted, S_READ, &fbp);
@@ -413,33 +410,35 @@
 			goto done;
 
 		blkp = (uchar_t *)fbp->fb_addr;
-		last_offset = (offset & MAXBMASK) + fbp->fb_count - 1;
+		last_offset = (offset & MAXBMASK) + fbp->fb_count;
 
 #define	rel_offset(offset) ((offset) & MAXBOFFSET)	/* index into blkp */
 
 		while (offset < last_offset) {
 			/*
-			 * Directory Entries cannot span sectors.
-			 * Unused bytes at the end of each sector are zeroed.
-			 * Therefore, detect this condition when the size
-			 * field of the directory entry is zero.
+			 * Very similar validation code is found in
+			 * process_dirblock(), hsfs_node.c.
+			 * For an explanation, see there.
+			 * It may make sense for the future to
+			 * "consolidate" the code in hs_parsedir(),
+			 * process_dirblock() and hsfs_readdir() into
+			 * a single utility function.
 			 */
 			hdlen = (int)((uchar_t)
 				HDE_DIR_LEN(&blkp[rel_offset(offset)]));
-			if (hdlen == 0) {
-				/* advance to next sector boundary */
-				offset = (offset & MAXHSMASK) + HS_SECTOR_SIZE;
-
+			if (hdlen < HDE_ROOT_DIR_REC_SIZE ||
+			    offset + hdlen > last_offset) {
 				/*
-				 * Have we reached the end of current block?
+				 * advance to next sector boundary
 				 */
-				if (offset > last_offset)
-					break;
-				else
-					continue;
+				offset = roundup(offset + 1, HS_SECTOR_SIZE);
+				if (hdlen)
+					hs_log_bogus_disk_warning(fsp,
+					    HSFS_ERR_TRAILING_JUNK, 0);
+
+				continue;
 			}
 
-			/* make sure this is nullified before  reading it */
 			bzero(&hd, sizeof (hd));
 
 			/*
@@ -509,7 +508,6 @@
 					hd.sym_link = (char *)NULL;
 				}
 			}
-
 			offset += hdlen;
 		}
 		fbrelse(fbp, S_READ);