blob: 400bf932631d79a8231e98ac56d72710357b5a7f [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string>
#include <fbl/unique_fd.h>
#include <gtest/gtest.h>
#include "src/starnix/tests/syscalls/cpp/test_helper.h"
namespace {
TEST(InotifyTest, DeleteWatchedDirWithoutDeleteSelfMaskEmitsIgnored) {
test_helper::ScopedTempDir temp_dir;
std::string watched_dir = temp_dir.path() + "/watched";
ASSERT_EQ(mkdir(watched_dir.c_str(), 0700), 0) << strerror(errno);
fbl::unique_fd fd(inotify_init1(IN_NONBLOCK | IN_CLOEXEC));
ASSERT_TRUE(fd.is_valid()) << strerror(errno);
int wd = inotify_add_watch(fd.get(), watched_dir.c_str(), IN_CREATE | IN_DELETE);
ASSERT_GE(wd, 0) << strerror(errno);
ASSERT_EQ(rmdir(watched_dir.c_str()), 0) << strerror(errno);
struct inotify_event event = {};
ssize_t bytes_read = read(fd.get(), &event, sizeof(event));
ASSERT_EQ(bytes_read, static_cast<ssize_t>(sizeof(event))) << strerror(errno);
EXPECT_EQ(event.wd, wd);
EXPECT_EQ(event.mask, static_cast<uint32_t>(IN_IGNORED));
// No further events should be queued, and the watch descriptor should already be removed.
EXPECT_EQ(read(fd.get(), &event, sizeof(event)), -1);
EXPECT_EQ(errno, EAGAIN);
EXPECT_EQ(inotify_rm_watch(fd.get(), wd), -1);
EXPECT_EQ(errno, EINVAL);
}
TEST(InotifyTest, DeleteWatchedDirWithDeleteSelfMaskEmitsDeleteSelfAndIgnored) {
test_helper::ScopedTempDir temp_dir;
std::string watched_dir = temp_dir.path() + "/watched";
ASSERT_EQ(mkdir(watched_dir.c_str(), 0700), 0) << strerror(errno);
fbl::unique_fd fd(inotify_init1(IN_NONBLOCK | IN_CLOEXEC));
ASSERT_TRUE(fd.is_valid()) << strerror(errno);
int wd = inotify_add_watch(fd.get(), watched_dir.c_str(), IN_CREATE | IN_DELETE_SELF);
ASSERT_GE(wd, 0) << strerror(errno);
ASSERT_EQ(rmdir(watched_dir.c_str()), 0) << strerror(errno);
struct inotify_event events[2] = {};
ssize_t bytes_read = read(fd.get(), events, sizeof(events));
ASSERT_EQ(bytes_read, static_cast<ssize_t>(sizeof(events))) << strerror(errno);
EXPECT_EQ(events[0].wd, wd);
EXPECT_EQ(events[0].mask, static_cast<uint32_t>(IN_DELETE_SELF));
EXPECT_EQ(events[1].wd, wd);
EXPECT_EQ(events[1].mask, static_cast<uint32_t>(IN_IGNORED));
EXPECT_EQ(inotify_rm_watch(fd.get(), wd), -1);
EXPECT_EQ(errno, EINVAL);
}
// Linux queues the child's self-events before the parent's IN_DELETE.
TEST(InotifyTest, DeleteWatchedDirEmitsSelfEventsBeforeParentDelete) {
test_helper::ScopedTempDir temp_dir;
std::string watched_dir = temp_dir.path() + "/subdir";
ASSERT_EQ(mkdir(watched_dir.c_str(), 0700), 0) << strerror(errno);
fbl::unique_fd fd(inotify_init1(IN_NONBLOCK | IN_CLOEXEC));
ASSERT_TRUE(fd.is_valid()) << strerror(errno);
int wd_parent = inotify_add_watch(fd.get(), temp_dir.path().c_str(), IN_DELETE);
ASSERT_GE(wd_parent, 0) << strerror(errno);
int wd_child = inotify_add_watch(fd.get(), watched_dir.c_str(), IN_DELETE_SELF);
ASSERT_GE(wd_child, 0) << strerror(errno);
ASSERT_EQ(rmdir(watched_dir.c_str()), 0) << strerror(errno);
alignas(struct inotify_event) char buf[4096] = {};
ssize_t bytes_read = read(fd.get(), buf, sizeof(buf));
ASSERT_GE(bytes_read, static_cast<ssize_t>(3 * sizeof(struct inotify_event))) << strerror(errno);
size_t offset = 0;
auto* ev0 = reinterpret_cast<struct inotify_event*>(buf + offset);
EXPECT_EQ(ev0->wd, wd_child);
EXPECT_EQ(ev0->mask, static_cast<uint32_t>(IN_DELETE_SELF));
offset += sizeof(struct inotify_event) + ev0->len;
ASSERT_LE(offset + sizeof(struct inotify_event), static_cast<size_t>(bytes_read));
auto* ev1 = reinterpret_cast<struct inotify_event*>(buf + offset);
EXPECT_EQ(ev1->wd, wd_child);
EXPECT_EQ(ev1->mask, static_cast<uint32_t>(IN_IGNORED));
offset += sizeof(struct inotify_event) + ev1->len;
ASSERT_LE(offset + sizeof(struct inotify_event), static_cast<size_t>(bytes_read));
auto* ev2 = reinterpret_cast<struct inotify_event*>(buf + offset);
EXPECT_EQ(ev2->wd, wd_parent);
EXPECT_EQ(ev2->mask, static_cast<uint32_t>(IN_DELETE | IN_ISDIR));
ASSERT_GT(ev2->len, 0u);
ASSERT_LE(offset + sizeof(struct inotify_event) + ev2->len, static_cast<size_t>(bytes_read));
ASSERT_LT(strnlen(ev2->name, ev2->len), ev2->len);
EXPECT_STREQ(ev2->name, "subdir");
offset += sizeof(struct inotify_event) + ev2->len;
EXPECT_EQ(offset, static_cast<size_t>(bytes_read));
}
// Linux emits IN_DELETE_SELF from fsnotify_inoderemove(), which runs only once the inode has no
// links left, so unlinking one of several hard links reports just the link count change and leaves
// the watch in place until the final link is removed.
void CheckHardLinkUnlinkOrder(bool watch_first) {
test_helper::ScopedTempDir temp_dir;
std::string first = temp_dir.path() + "/first";
std::string second = temp_dir.path() + "/second";
fbl::unique_fd created(open(first.c_str(), O_CREAT | O_RDWR, 0600));
ASSERT_TRUE(created.is_valid()) << strerror(errno);
created.reset();
ASSERT_EQ(link(first.c_str(), second.c_str()), 0) << strerror(errno);
fbl::unique_fd fd(inotify_init1(IN_NONBLOCK | IN_CLOEXEC));
ASSERT_TRUE(fd.is_valid()) << strerror(errno);
const std::string& watched = watch_first ? first : second;
int wd = inotify_add_watch(fd.get(), watched.c_str(), IN_ATTRIB | IN_DELETE_SELF);
ASSERT_GE(wd, 0) << strerror(errno);
ASSERT_EQ(unlink(first.c_str()), 0) << strerror(errno);
struct inotify_event event = {};
ASSERT_EQ(read(fd.get(), &event, sizeof(event)), static_cast<ssize_t>(sizeof(event)))
<< strerror(errno);
EXPECT_EQ(event.wd, wd);
EXPECT_EQ(event.mask, static_cast<uint32_t>(IN_ATTRIB));
EXPECT_EQ(read(fd.get(), &event, sizeof(event)), -1);
EXPECT_EQ(errno, EAGAIN);
// Unlinking the remaining link destroys the inode and emits IN_ATTRIB, IN_DELETE_SELF, and
// IN_IGNORED.
ASSERT_EQ(unlink(second.c_str()), 0) << strerror(errno);
struct inotify_event final_events[3] = {};
ASSERT_EQ(read(fd.get(), final_events, sizeof(final_events)),
static_cast<ssize_t>(sizeof(final_events)))
<< strerror(errno);
EXPECT_EQ(final_events[0].wd, wd);
EXPECT_EQ(final_events[0].mask, static_cast<uint32_t>(IN_ATTRIB));
EXPECT_EQ(final_events[1].wd, wd);
EXPECT_EQ(final_events[1].mask, static_cast<uint32_t>(IN_DELETE_SELF));
EXPECT_EQ(final_events[2].wd, wd);
EXPECT_EQ(final_events[2].mask, static_cast<uint32_t>(IN_IGNORED));
EXPECT_EQ(inotify_rm_watch(fd.get(), wd), -1);
EXPECT_EQ(errno, EINVAL);
}
TEST(InotifyTest, UnlinkOneOfTwoHardLinksDoesNotEmitDeleteSelf) {
ASSERT_NO_FATAL_FAILURE(CheckHardLinkUnlinkOrder(/*watch_first=*/false));
}
// TODO(https://fxbug.dev/565365826): Unlinking the watched link first currently pins its
// `DirEntryHandle` in `InotifyState::watches`, preventing `DELETE_SELF` when the surviving link is
// unlinked.
TEST(InotifyTest, UnlinkWatchedHardLinkFirstEmitsDeleteSelfOnLastUnlink) {
ASSERT_NO_FATAL_FAILURE(CheckHardLinkUnlinkOrder(/*watch_first=*/true));
}
} // namespace