Skip to content

Commit a65eb6a

Browse files
committed
Merge pull request #12771 from tanyifeng:tmpfs-otrunc-mtime
PiperOrigin-RevId: 892618235
2 parents 82275ab + d5da431 commit a65eb6a

6 files changed

Lines changed: 141 additions & 38 deletions

File tree

pkg/sentry/fsimpl/tmpfs/filesystem.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,8 @@ func (d *dentry) open(ctx context.Context, rp *vfs.ResolvingPath, opts *vfs.Open
468468
fd.vfsfd.DecRef(ctx)
469469
return nil, err
470470
}
471-
_, err := impl.truncate(0)
471+
// truncate updates mtime/ctime internally.
472+
err := impl.truncate(0)
472473
mnt.EndWrite()
473474
if err != nil {
474475
fd.vfsfd.DecRef(ctx)

pkg/sentry/fsimpl/tmpfs/regular_file.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,16 @@ func NewMemfd(ctx context.Context, creds *auth.Credentials, mount *vfs.Mount, al
173173
return &fd.vfsfd, nil
174174
}
175175

176-
// truncate grows or shrinks the file to the given size. It returns true if the
177-
// file size was updated.
178-
func (rf *regularFile) truncate(newSize uint64) (bool, error) {
176+
// truncate grows or shrinks the file to the given size. It unconditionally
177+
// updates mtime and ctime.
178+
func (rf *regularFile) truncate(newSize uint64) error {
179179
rf.inode.mu.Lock()
180180
defer rf.inode.mu.Unlock()
181-
return rf.truncateLocked(newSize)
181+
if err := rf.truncateNoTimeUpdateLocked(newSize); err != nil {
182+
return err
183+
}
184+
rf.inode.touchCMtimeLocked()
185+
return nil
182186
}
183187

184188
// Preconditions:
@@ -194,26 +198,29 @@ func (rf *regularFile) growLocked(newSize uint64) error {
194198
return nil
195199
}
196200

201+
// truncateNoTimeUpdateLocked grows or shrinks the file to the given size.
202+
// Callers are responsible for updating timestamps.
203+
//
197204
// Preconditions: rf.inode.mu must be held.
198-
func (rf *regularFile) truncateLocked(newSize uint64) (bool, error) {
205+
func (rf *regularFile) truncateNoTimeUpdateLocked(newSize uint64) error {
199206
oldSize := rf.size.RacyLoad()
200207
if newSize == oldSize {
201208
// Nothing to do.
202-
return false, nil
209+
return nil
203210
}
204211

205212
// Need to hold inode.mu and dataMu while modifying size.
206213
rf.dataMu.Lock()
207214
if newSize > oldSize {
208215
err := rf.growLocked(newSize)
209216
rf.dataMu.Unlock()
210-
return err == nil, err
217+
return err
211218
}
212219

213220
// We are shrinking the file. First check if this is allowed.
214221
if rf.seals&linux.F_SEAL_SHRINK != 0 {
215222
rf.dataMu.Unlock()
216-
return false, linuxerr.EPERM
223+
return linuxerr.EPERM
217224
}
218225

219226
rf.size.Store(newSize)
@@ -238,7 +245,7 @@ func (rf *regularFile) truncateLocked(newSize uint64) (bool, error) {
238245
decPages := rf.data.Truncate(newSize, rf.inode.fs.mf)
239246
rf.dataMu.Unlock()
240247
rf.inode.fs.unaccountPages(decPages)
241-
return true, nil
248+
return nil
242249
}
243250

244251
// AddMapping implements memmap.Mappable.AddMapping.

pkg/sentry/fsimpl/tmpfs/regular_file_test.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -324,26 +324,4 @@ func TestTruncate(t *testing.T) {
324324
if got := statAfterTruncateUp.Ctime.ToNsec(); got <= statAfterTruncateDown.Ctime.ToNsec() {
325325
t.Errorf("fd.Stat got Ctime %v, want > %v", got, stat.Ctime)
326326
}
327-
328-
// Truncate to the current size.
329-
newSize = statAfterTruncateUp.Size
330-
if err := fd.SetStat(ctx, vfs.SetStatOptions{
331-
Stat: linux.Statx{
332-
Mask: linux.STATX_SIZE,
333-
Size: newSize,
334-
},
335-
}); err != nil {
336-
t.Errorf("fd.Truncate failed: %v", err)
337-
}
338-
statAfterTruncateNoop, err := fd.Stat(ctx, sizeStatOpts)
339-
if err != nil {
340-
t.Fatalf("fd.Stat failed: %v", err)
341-
}
342-
// Mtime and Ctime should not be bumped, since operation is a noop.
343-
if got := statAfterTruncateNoop.Mtime.ToNsec(); got != statAfterTruncateUp.Mtime.ToNsec() {
344-
t.Errorf("fd.Stat got Mtime %v, want %v", got, statAfterTruncateUp.Mtime)
345-
}
346-
if got := statAfterTruncateNoop.Ctime.ToNsec(); got != statAfterTruncateUp.Ctime.ToNsec() {
347-
t.Errorf("fd.Stat got Ctime %v, want %v", got, statAfterTruncateUp.Ctime)
348-
}
349327
}

pkg/sentry/fsimpl/tmpfs/tmpfs.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -704,14 +704,11 @@ func (i *inode) setStat(ctx context.Context, creds *auth.Credentials, opts *vfs.
704704
if mask&linux.STATX_SIZE != 0 {
705705
switch impl := i.impl.(type) {
706706
case *regularFile:
707-
updated, err := impl.truncateLocked(stat.Size)
708-
if err != nil {
707+
if err := impl.truncateNoTimeUpdateLocked(stat.Size); err != nil {
709708
return err
710709
}
711-
if updated {
712-
needsMtimeBump = true
713-
needsCtimeBump = true
714-
}
710+
needsMtimeBump = true
711+
needsCtimeBump = true
715712
case *directory:
716713
return linuxerr.EISDIR
717714
default:

test/syscalls/linux/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,6 +1596,7 @@ cc_binary(
15961596
"//test/util:thread_util",
15971597
"@com_google_absl//absl/memory",
15981598
"@com_google_absl//absl/strings",
1599+
"@com_google_absl//absl/time",
15991600
],
16001601
)
16011602

test/syscalls/linux/open.cc

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
#include <unistd.h>
2121

2222
#include <cstdlib>
23+
#include <ctime>
2324
#include <memory>
2425

2526
#include "gmock/gmock.h"
2627
#include "gtest/gtest.h"
2728
#include "absl/memory/memory.h"
2829
#include "absl/strings/string_view.h"
30+
#include "absl/time/clock.h"
31+
#include "absl/time/time.h"
2932
#include "test/syscalls/linux/file_base.h"
3033
#include "test/util/capability_util.h"
3134
#include "test/util/cleanup.h"
@@ -542,6 +545,122 @@ TEST_F(OpenTest, OPathWithODirectory) {
542545
SyscallFailsWithErrno(ENOTDIR));
543546
}
544547

548+
// Truncate operations that don't change file size should still update mtime
549+
// and ctime. Tests truncation via open(O_TRUNC), ftruncate(2), and truncate(2).
550+
TEST_F(OpenTest, TruncateNoSizeChangeUpdatesTimestamps) {
551+
// Create three zero-length files and record their initial timestamps.
552+
auto path1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
553+
auto path2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
554+
auto path3 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
555+
556+
// When running on gVisor, only test on tmpfs. We don't know how the gofer
557+
// server implements open(O_TRUNC), ftruncate(2), and truncate(2). If it
558+
// uses truncate(2) under the hood and the underlying filesystem is host
559+
// tmpfs, then this test will fail.
560+
SKIP_IF(IsRunningOnGvisor() &&
561+
!ASSERT_NO_ERRNO_AND_VALUE(IsTmpfs(path1.path())));
562+
563+
struct stat before1 = {}, before2 = {}, before3 = {};
564+
ASSERT_THAT(stat(path1.path().c_str(), &before1), SyscallSucceeds());
565+
ASSERT_THAT(stat(path2.path().c_str(), &before2), SyscallSucceeds());
566+
ASSERT_THAT(stat(path3.path().c_str(), &before3), SyscallSucceeds());
567+
EXPECT_EQ(before1.st_size, 0);
568+
EXPECT_EQ(before2.st_size, 0);
569+
EXPECT_EQ(before3.st_size, 0);
570+
571+
const auto ts_gt = [](const struct timespec& a, const struct timespec& b) {
572+
return a.tv_sec > b.tv_sec ||
573+
(a.tv_sec == b.tv_sec && a.tv_nsec > b.tv_nsec);
574+
};
575+
576+
absl::SleepFor(absl::Milliseconds(10));
577+
578+
// Test 1: open(O_TRUNC) on zero-length file.
579+
const FileDescriptor fd1 =
580+
ASSERT_NO_ERRNO_AND_VALUE(Open(path1.path(), O_WRONLY | O_TRUNC));
581+
struct stat after1 = {};
582+
ASSERT_THAT(fstat(fd1.get(), &after1), SyscallSucceeds());
583+
EXPECT_EQ(after1.st_size, 0);
584+
EXPECT_TRUE(ts_gt(after1.st_mtim, before1.st_mtim));
585+
EXPECT_TRUE(ts_gt(after1.st_ctim, before1.st_ctim));
586+
587+
// Test 2: ftruncate(2) to same size (0).
588+
const FileDescriptor fd2 =
589+
ASSERT_NO_ERRNO_AND_VALUE(Open(path2.path(), O_WRONLY));
590+
ASSERT_THAT(ftruncate(fd2.get(), 0), SyscallSucceeds());
591+
struct stat after2 = {};
592+
ASSERT_THAT(fstat(fd2.get(), &after2), SyscallSucceeds());
593+
EXPECT_EQ(after2.st_size, 0);
594+
EXPECT_TRUE(ts_gt(after2.st_mtim, before2.st_mtim));
595+
EXPECT_TRUE(ts_gt(after2.st_ctim, before2.st_ctim));
596+
597+
// Test 3: truncate(2) to same size (0). Note that Linux tmpfs does NOT
598+
// update mtime/ctime for truncate(2) same-size, but ext4 does. gVisor
599+
// unconditionally updates for simplicity. Do not test this on native tmpfs.
600+
if (IsRunningOnGvisor() ||
601+
!ASSERT_NO_ERRNO_AND_VALUE(IsTmpfs(path3.path()))) {
602+
ASSERT_THAT(truncate(path3.path().c_str(), 0), SyscallSucceeds());
603+
struct stat after3 = {};
604+
ASSERT_THAT(stat(path3.path().c_str(), &after3), SyscallSucceeds());
605+
EXPECT_EQ(after3.st_size, 0);
606+
EXPECT_TRUE(ts_gt(after3.st_mtim, before3.st_mtim));
607+
EXPECT_TRUE(ts_gt(after3.st_ctim, before3.st_ctim));
608+
}
609+
}
610+
611+
// Truncate operations that change file size should always update mtime and
612+
// ctime. Tests truncation via open(O_TRUNC), ftruncate(2), and truncate(2).
613+
TEST_F(OpenTest, TruncateSizeChangeUpdatesTimestamps) {
614+
auto path1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith(
615+
GetAbsoluteTestTmpdir(), "initial data 1", 0644));
616+
auto path2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith(
617+
GetAbsoluteTestTmpdir(), "initial data 2", 0644));
618+
auto path3 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith(
619+
GetAbsoluteTestTmpdir(), "initial data 3", 0644));
620+
621+
struct stat before1 = {}, before2 = {}, before3 = {};
622+
ASSERT_THAT(stat(path1.path().c_str(), &before1), SyscallSucceeds());
623+
ASSERT_THAT(stat(path2.path().c_str(), &before2), SyscallSucceeds());
624+
ASSERT_THAT(stat(path3.path().c_str(), &before3), SyscallSucceeds());
625+
EXPECT_GT(before1.st_size, 0);
626+
EXPECT_GT(before2.st_size, 0);
627+
EXPECT_GT(before3.st_size, 0);
628+
629+
const auto ts_gt = [](const struct timespec& a, const struct timespec& b) {
630+
return a.tv_sec > b.tv_sec ||
631+
(a.tv_sec == b.tv_sec && a.tv_nsec > b.tv_nsec);
632+
};
633+
634+
absl::SleepFor(absl::Milliseconds(10));
635+
636+
// Test 1: open(O_TRUNC) on non-zero-length file.
637+
const FileDescriptor fd1 =
638+
ASSERT_NO_ERRNO_AND_VALUE(Open(path1.path(), O_WRONLY | O_TRUNC));
639+
struct stat after1 = {};
640+
ASSERT_THAT(fstat(fd1.get(), &after1), SyscallSucceeds());
641+
EXPECT_EQ(after1.st_size, 0);
642+
EXPECT_TRUE(ts_gt(after1.st_mtim, before1.st_mtim));
643+
EXPECT_TRUE(ts_gt(after1.st_ctim, before1.st_ctim));
644+
645+
// Test 2: ftruncate(2) to smaller size.
646+
const FileDescriptor fd2 =
647+
ASSERT_NO_ERRNO_AND_VALUE(Open(path2.path(), O_WRONLY));
648+
ASSERT_THAT(ftruncate(fd2.get(), 5), SyscallSucceeds());
649+
struct stat after2 = {};
650+
ASSERT_THAT(fstat(fd2.get(), &after2), SyscallSucceeds());
651+
EXPECT_EQ(after2.st_size, 5);
652+
EXPECT_TRUE(ts_gt(after2.st_mtim, before2.st_mtim));
653+
EXPECT_TRUE(ts_gt(after2.st_ctim, before2.st_ctim));
654+
655+
// Test 3: truncate(2) to 0.
656+
ASSERT_THAT(truncate(path3.path().c_str(), 0), SyscallSucceeds());
657+
struct stat after3 = {};
658+
ASSERT_THAT(stat(path3.path().c_str(), &after3), SyscallSucceeds());
659+
EXPECT_EQ(after3.st_size, 0);
660+
EXPECT_TRUE(ts_gt(after3.st_mtim, before3.st_mtim));
661+
EXPECT_TRUE(ts_gt(after3.st_ctim, before3.st_ctim));
662+
}
663+
545664
} // namespace
546665

547666
} // namespace testing

0 commit comments

Comments
 (0)