summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLOLi <[email protected]>2017-11-13 18:24:26 +0100
committerTony Hutter <[email protected]>2017-11-21 13:11:29 -0600
commitfedc1d96a8d8251237916fdf230e981bc396aa02 (patch)
treee33191d4f7afb811854f4ddf1dc8f67b14b4cb63
parent59511072b47175bc3b757b4cc27f30b752f59662 (diff)
Fix truncate(2) mtime and ctime handling
On Linux, ftruncate(2) always changes the file timestamps, even if the file size is not changed. However, in case of a successfull truncate(2), the timestamps are updated only if the file size changes. This translates to the VFS calling the ZFS Posix Layer "setattr" function (zpl_setattr) with ATTR_MTIME and ATTR_CTIME unconditionally set on the iattr mask only when doing a ftruncate(2), while the truncate(2) is left to the filesystem implementation to be dealt with. This behaviour is consistent with POSIX:2004/SUSv3 specifications where there's no explicit requirement for file size changes to update the timestamps only for ftruncate(2): http://pubs.opengroup.org/onlinepubs/009695399/functions/truncate.html http://pubs.opengroup.org/onlinepubs/009695399/functions/ftruncate.html This has been later updated in POSIX:2008/SUSv4 where, for both truncate(2)/ftruncate(2), there's no mention of this size change requirement: http://austingroupbugs.net/view.php?id=489 http://pubs.opengroup.org/onlinepubs/9699919799/functions/truncate.html http://pubs.opengroup.org/onlinepubs/9699919799/functions/ftruncate.html Unfortunately the Linux VFS is still calling into the ZPL without ATTR_MTIME/ATTR_CTIME set in the truncate(2) case: we fix this by explicitly updating the timestamps when detecting the ATTR_SIZE bit, which is always set in do_truncate(), on the iattr mask. Reviewed-by: Brian Behlendorf <[email protected]> Signed-off-by: loli10K <[email protected]> Closes #6811 Closes #6819
-rw-r--r--module/zfs/zfs_vnops.c4
-rw-r--r--tests/runfiles/linux.run3
-rw-r--r--tests/zfs-tests/include/math.shlib30
-rw-r--r--tests/zfs-tests/tests/functional/truncate/.gitignore1
-rw-r--r--tests/zfs-tests/tests/functional/truncate/Makefile.am11
-rw-r--r--tests/zfs-tests/tests/functional/truncate/truncate_test.c103
-rwxr-xr-xtests/zfs-tests/tests/functional/truncate/truncate_timestamps.ksh74
7 files changed, 222 insertions, 4 deletions
diff --git a/module/zfs/zfs_vnops.c b/module/zfs/zfs_vnops.c
index a88f2482e..6a1dab5c9 100644
--- a/module/zfs/zfs_vnops.c
+++ b/module/zfs/zfs_vnops.c
@@ -3155,7 +3155,7 @@ top:
&atime, sizeof (atime));
}
- if (mask & ATTR_MTIME) {
+ if (mask & (ATTR_MTIME | ATTR_SIZE)) {
ZFS_TIME_ENCODE(&vap->va_mtime, mtime);
ZTOI(zp)->i_mtime = timespec_trunc(vap->va_mtime,
ZTOI(zp)->i_sb->s_time_gran);
@@ -3164,7 +3164,7 @@ top:
mtime, sizeof (mtime));
}
- if (mask & ATTR_CTIME) {
+ if (mask & (ATTR_CTIME | ATTR_SIZE)) {
ZFS_TIME_ENCODE(&vap->va_ctime, ctime);
ZTOI(zp)->i_ctime = timespec_trunc(vap->va_ctime,
ZTOI(zp)->i_sb->s_time_gran);
diff --git a/tests/runfiles/linux.run b/tests/runfiles/linux.run
index 5583a2554..7d6d13b4b 100644
--- a/tests/runfiles/linux.run
+++ b/tests/runfiles/linux.run
@@ -537,7 +537,8 @@ tests = ['threadsappend_001_pos']
tests = ['tmpfile_001_pos', 'tmpfile_002_pos', 'tmpfile_003_pos']
[tests/functional/truncate]
-tests = ['truncate_001_pos', 'truncate_002_pos']
+tests = ['truncate_001_pos', 'truncate_002_pos', 'truncate_timestamps']
+tags = ['functional', 'truncate']
[tests/functional/upgrade]
tests = [ 'upgrade_userobj_001_pos' ]
diff --git a/tests/zfs-tests/include/math.shlib b/tests/zfs-tests/include/math.shlib
index 4ed11fb4b..66658cdda 100644
--- a/tests/zfs-tests/include/math.shlib
+++ b/tests/zfs-tests/include/math.shlib
@@ -66,3 +66,33 @@ function to_bytes
return 0
}
+
+#
+# Verify $a is equal to $b, otherwise raise an error specifying
+# the $type of values being compared
+#
+function verify_eq # <a> <b> <type>
+{
+ typeset a=$1
+ typeset b=$2
+ typeset type=$3
+
+ if [[ $a -ne $b ]]; then
+ log_fail "Compared $type should be equal: $a != $b"
+ fi
+}
+
+#
+# Verify $a is not equal to $b, otherwise raise an error specifying
+# the $type of values being compared
+#
+function verify_ne # <a> <b> <type>
+{
+ typeset a=$1
+ typeset b=$2
+ typeset type=$3
+
+ if [[ $a -eq $b ]]; then
+ log_fail "Compared $type should be not equal: $a == $b"
+ fi
+}
diff --git a/tests/zfs-tests/tests/functional/truncate/.gitignore b/tests/zfs-tests/tests/functional/truncate/.gitignore
new file mode 100644
index 000000000..f28d93573
--- /dev/null
+++ b/tests/zfs-tests/tests/functional/truncate/.gitignore
@@ -0,0 +1 @@
+/truncate_test
diff --git a/tests/zfs-tests/tests/functional/truncate/Makefile.am b/tests/zfs-tests/tests/functional/truncate/Makefile.am
index 16cadf29d..0071e8f87 100644
--- a/tests/zfs-tests/tests/functional/truncate/Makefile.am
+++ b/tests/zfs-tests/tests/functional/truncate/Makefile.am
@@ -1,7 +1,16 @@
+include $(top_srcdir)/config/Rules.am
+
pkgdatadir = $(datadir)/@PACKAGE@/zfs-tests/tests/functional/truncate
+
dist_pkgdata_SCRIPTS = \
setup.ksh \
cleanup.ksh \
truncate.cfg \
truncate_001_pos.ksh \
- truncate_002_pos.ksh
+ truncate_002_pos.ksh \
+ truncate_timestamps.ksh
+
+pkgexecdir = $(datadir)/@PACKAGE@/zfs-tests/tests/functional/truncate
+
+pkgexec_PROGRAMS = truncate_test
+truncate_test_SOURCES = truncate_test.c
diff --git a/tests/zfs-tests/tests/functional/truncate/truncate_test.c b/tests/zfs-tests/tests/functional/truncate/truncate_test.c
new file mode 100644
index 000000000..3e277e865
--- /dev/null
+++ b/tests/zfs-tests/tests/functional/truncate/truncate_test.c
@@ -0,0 +1,103 @@
+/*
+ * This file and its contents are supplied under the terms of the
+ * Common Development and Distribution License ("CDDL"), version 1.0.
+ * You may only use this file in accordance with the terms of version
+ * 1.0 of the CDDL.
+ *
+ * A full copy of the text of the CDDL should have accompanied this
+ * source. A copy of the CDDL is also available via the Internet at
+ * http://www.illumos.org/license/CDDL.
+ */
+
+/*
+ * Copyright (c) 2012, 2014 by Delphix. All rights reserved.
+ * Copyright 2017, loli10K <[email protected]>. All rights reserved.
+ */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#define FSIZE 256*1024*1024
+
+static long fsize = FSIZE;
+static int errflag = 0;
+static char *filename = NULL;
+static int ftruncflag = 0;
+
+static void parse_options(int argc, char *argv[]);
+
+static void
+usage(char *execname)
+{
+ (void) fprintf(stderr,
+ "usage: %s [-s filesize] [-f] /path/to/file\n", execname);
+ (void) exit(1);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int fd;
+
+ parse_options(argc, argv);
+
+ if (ftruncflag) {
+ fd = open(filename, O_RDWR|O_CREAT, 0666);
+ if (fd < 0) {
+ perror("open");
+ return (1);
+ }
+ if (ftruncate(fd, fsize) < 0) {
+ perror("ftruncate");
+ return (1);
+ }
+ if (close(fd)) {
+ perror("close");
+ return (1);
+ }
+ } else {
+ if (truncate(filename, fsize) < 0) {
+ perror("truncate");
+ return (1);
+ }
+ }
+ return (0);
+}
+
+static void
+parse_options(int argc, char *argv[])
+{
+ int c;
+ extern char *optarg;
+ extern int optind, optopt;
+
+ while ((c = getopt(argc, argv, "s:f")) != -1) {
+ switch (c) {
+ case 's':
+ fsize = atoi(optarg);
+ break;
+ case 'f':
+ ftruncflag++;
+ break;
+ case ':':
+ (void) fprintf(stderr,
+ "Option -%c requires an operand\n", optopt);
+ errflag++;
+ break;
+ }
+ if (errflag) {
+ (void) usage(argv[0]);
+ }
+ }
+
+ if (argc <= optind) {
+ (void) fprintf(stderr, "No filename specified\n");
+ usage(argv[0]);
+ }
+ filename = argv[optind];
+}
diff --git a/tests/zfs-tests/tests/functional/truncate/truncate_timestamps.ksh b/tests/zfs-tests/tests/functional/truncate/truncate_timestamps.ksh
new file mode 100755
index 000000000..c365c7415
--- /dev/null
+++ b/tests/zfs-tests/tests/functional/truncate/truncate_timestamps.ksh
@@ -0,0 +1,74 @@
+#!/bin/ksh -p
+#
+# This file and its contents are supplied under the terms of the
+# Common Development and Distribution License ("CDDL"), version 1.0.
+# You may only use this file in accordance with the terms of version
+# 1.0 of the CDDL.
+#
+# A full copy of the text of the CDDL should have accompanied this
+# source. A copy of the CDDL is also available via the Internet at
+# http://www.illumos.org/license/CDDL.
+#
+
+#
+# Copyright 2017, loli10K <[email protected]>. All rights reserved.
+#
+
+. $STF_SUITE/tests/functional/truncate/truncate.cfg
+. $STF_SUITE/include/libtest.shlib
+. $STF_SUITE/include/math.shlib
+
+#
+# DESCRIPTION:
+# Ensure both truncate(2)/ftruncate(2) update target file mtime/ctime attributes
+#
+# STRATEGY:
+# 1. Create a file
+# 2. Truncate the file
+# 3. Verify both mtime/ctime are updated
+# 4. Rinse and repeat for both truncate(2) and ftruncate(2) with various sizes
+#
+
+verify_runnable "both"
+
+function verify_truncate # <filename> <filesize> <option>
+{
+ typeset filename="$1"
+ typeset -i size="$2"
+ typeset option="$3"
+
+ log_must mkfile $sizeavg $filename # always start with $sizeavg
+ typeset -i timestm="$(stat -c %Y $filename)"
+ typeset -i timestc="$(stat -c %Z $filename)"
+ log_must sleep 1
+ log_must $STF_SUITE/tests/functional/truncate/truncate_test -s $size $filename $option
+ verify_eq $size "$(stat -c %s $filename)" "size"
+ verify_ne $timestm "$(stat -c %Y $filename)" "mtime"
+ verify_ne $timestc "$(stat -c %Z $filename)" "ctime"
+ log_must rm -f $filename
+}
+
+function cleanup
+{
+ [[ -f $truncfile ]] && rm -f $truncfile
+}
+
+log_assert "Ensure both truncate(2)/ftruncate(2) update target file timestamps"
+log_onexit cleanup
+
+truncfile="$TESTDIR/truncate.$$"
+sizemin="123"
+sizeavg="$((256*1024))"
+sizemax="$((1024*1024))"
+
+# truncate(2)
+verify_truncate $truncfile $sizemin ""
+verify_truncate $truncfile $sizeavg ""
+verify_truncate $truncfile $sizemax ""
+
+# ftruncate(2)
+verify_truncate $truncfile $sizemin "-f"
+verify_truncate $truncfile $sizeavg "-f"
+verify_truncate $truncfile $sizemax "-f"
+
+log_pass "Successful truncation correctly update timestamps"