diff mbox

[v7,5/6] fs/dcache: Allow optional enforcement of negative dentry limit

Message ID 1531413965-5401-6-git-send-email-longman@redhat.com (mailing list archive)
State New, archived
Headers show

Commit Message

Waiman Long July 12, 2018, 4:46 p.m. UTC
If a rogue application that generates a large number of negative dentries
is running, it is possible that negative dentries will use up most of
the available memory in the system when that application is not under
the control of a memory cgroup that limit kernel memory.

The lack of available memory may significantly impact the operation
of other applications running in the system. It will slow down system
performance and may even work as part of a DoS attack on the system.

To allow system administrators the option to prevent this extreme
situation from happening, a new "neg-dentry-enforce" sysctl parameter
is now added which can be set to to enforce the negative dentry soft
limit set in "neg-dentry-limit" so that it becomes a hard limit. When
the limit is enforced, extra negative dentries that exceed the limit
will be killed after use instead of leaving them in the LRU.

Note that negative dentry killing happens when the global negative
dentry free pool is depleted even if there are space in other percpu
negative dentry counts for more.

Signed-off-by: Waiman Long <longman@redhat.com>
---
 Documentation/sysctl/fs.txt | 10 +++++++++
 fs/dcache.c                 | 51 ++++++++++++++++++++++++++++++++++++---------
 include/linux/dcache.h      |  2 ++
 kernel/sysctl.c             |  9 ++++++++
 4 files changed, 62 insertions(+), 10 deletions(-)
diff mbox

Patch

diff --git a/Documentation/sysctl/fs.txt b/Documentation/sysctl/fs.txt
index 6fd43b3..b073d9a 100644
--- a/Documentation/sysctl/fs.txt
+++ b/Documentation/sysctl/fs.txt
@@ -32,6 +32,7 @@  Currently, these files are in /proc/sys/fs:
 - nr_open
 - overflowuid
 - overflowgid
+- neg-dentry-enforce
 - neg-dentry-limit
 - pipe-user-pages-hard
 - pipe-user-pages-soft
@@ -169,6 +170,15 @@  The default is 65534.
 
 ==============================================================
 
+neg-dentry-enforce:
+
+The file neg-dentry-enforce, if present, contains a boolean flag (0 or
+1) indicating if the negative dentries limit set by the "neg-dentry-limit"
+sysctl parameter should be enforced or not.  If enforced, excess negative
+dentries over the limit will be killed immediately after use.
+
+==============================================================
+
 neg-dentry-limit:
 
 This integer value specifies a soft limit on the total number of
diff --git a/fs/dcache.c b/fs/dcache.c
index b2c1585..843c8be 100644
--- a/fs/dcache.c
+++ b/fs/dcache.c
@@ -145,6 +145,8 @@  struct dentry_stat_t dentry_stat = {
 static int neg_dentry_limit_old;
 int neg_dentry_limit;
 EXPORT_SYMBOL_GPL(neg_dentry_limit);
+int neg_dentry_enforce;	/* Enforce the negative dentry limit */
+EXPORT_SYMBOL_GPL(neg_dentry_enforce);
 
 static long neg_dentry_percpu_limit __read_mostly;
 static long neg_dentry_nfree_init __read_mostly; /* Free pool initial value */
@@ -308,7 +310,7 @@  static long __neg_dentry_nfree_dec(long cnt)
 	return cnt;
 }
 
-static noinline void neg_dentry_inc_slowpath(struct dentry *dentry)
+static noinline int neg_dentry_inc_slowpath(struct dentry *dentry)
 {
 	long cnt = 0, *pcnt;
 	unsigned long current_time;
@@ -330,6 +332,24 @@  static noinline void neg_dentry_inc_slowpath(struct dentry *dentry)
 		goto out;
 
 	/*
+	 * Kill the dentry by setting the DCACHE_KILL_NEGATIVE flag and
+	 * dec the negative dentry count if the enforcing option is on.
+	 */
+	if (neg_dentry_enforce) {
+		dentry->d_flags |= DCACHE_KILL_NEGATIVE;
+		this_cpu_dec(nr_dentry_neg);
+
+		/*
+		 * When the dentry is not put into the LRU, we
+		 * need to keep the reference count to 1 to
+		 * avoid problem when killing it.
+		 */
+		WARN_ON_ONCE(dentry->d_lockref.count);
+		dentry->d_lockref.count = 1;
+		return -1; /* Kill the dentry now */
+	}
+
+	/*
 	 * Put out a warning every minute or so if there are just too many
 	 * negative dentries.
 	 */
@@ -354,27 +374,28 @@  static noinline void neg_dentry_inc_slowpath(struct dentry *dentry)
 	 */
 	cnt = get_nr_dentry_neg();
 	pr_warn("Warning: Too many negative dentries (%ld). "
-		"This warning can be disabled by writing 0 to \"fs/neg-dentry-limit\" or increasing the limit.\n",
+		"This warning can be disabled by writing 0 to \"fs/neg-dentry-limit\", increasing the limit or writing 1 to \"fs/neg-dentry-enforce\".\n",
 		cnt);
 out:
-	return;
+	return 0;
 }
 
 /*
  * Increment negative dentry count if applicable.
+ * Return: 0 on success, -1 to kill it.
  */
-static void neg_dentry_inc(struct dentry *dentry)
+static int neg_dentry_inc(struct dentry *dentry)
 {
 	if (!static_key_false(&limit_neg_key)) {
 		this_cpu_inc(nr_dentry_neg);
-		return;
+		return 0;
 	}
 
 	if (likely(this_cpu_inc_return(nr_dentry_neg) <=
 		   neg_dentry_percpu_limit))
-		return;
+		return 0;
 
-	neg_dentry_inc_slowpath(dentry);
+	return neg_dentry_inc_slowpath(dentry);
 }
 
 /*
@@ -623,7 +644,11 @@  static void d_lru_add(struct dentry *dentry)
 	if (d_is_negative(dentry)) {
 		if (dentry->d_flags & DCACHE_NEW_NEGATIVE) {
 			dentry->d_flags &= ~DCACHE_NEW_NEGATIVE;
-			neg_dentry_inc(dentry);
+			if (unlikely(neg_dentry_inc(dentry) < 0)) {
+				this_cpu_dec(nr_dentry_unused);
+				dentry->d_flags &= ~DCACHE_LRU_LIST;
+				return;	/* To be killed */
+			}
 
 			/*
 			 * Add the negative dentry to the head once, it
@@ -878,16 +903,22 @@  static inline bool retain_dentry(struct dentry *dentry)
 	if (unlikely(dentry->d_flags & DCACHE_DISCONNECTED))
 		return false;
 
+	if (unlikely(dentry->d_flags & DCACHE_KILL_NEGATIVE))
+		return false;
+
 	if (unlikely(dentry->d_flags & DCACHE_OP_DELETE)) {
 		if (dentry->d_op->d_delete(dentry))
 			return false;
 	}
 	/* retain; LRU fodder */
 	dentry->d_lockref.count--;
-	if (unlikely(!(dentry->d_flags & DCACHE_LRU_LIST)))
+	if (unlikely(!(dentry->d_flags & DCACHE_LRU_LIST))) {
 		d_lru_add(dentry);
-	else if (unlikely(!(dentry->d_flags & DCACHE_REFERENCED)))
+		if (unlikely(dentry->d_flags & DCACHE_KILL_NEGATIVE))
+			return false;
+	} else if (unlikely(!(dentry->d_flags & DCACHE_REFERENCED))) {
 		dentry->d_flags |= DCACHE_REFERENCED;
+	}
 	return true;
 }
 
diff --git a/include/linux/dcache.h b/include/linux/dcache.h
index 4216eca..934a6d9 100644
--- a/include/linux/dcache.h
+++ b/include/linux/dcache.h
@@ -219,6 +219,7 @@  struct dentry_operations {
 
 #define DCACHE_PAR_LOOKUP		0x10000000 /* being looked up (with parent locked shared) */
 #define DCACHE_DENTRY_CURSOR		0x20000000
+#define DCACHE_KILL_NEGATIVE		0x40000000 /* Kill negative dentry */
 
 extern seqlock_t rename_lock;
 
@@ -615,5 +616,6 @@  struct name_snapshot {
  * Negative dentry related declarations.
  */
 extern int neg_dentry_limit;
+extern int neg_dentry_enforce;
 
 #endif	/* __LINUX_DCACHE_H */
diff --git a/kernel/sysctl.c b/kernel/sysctl.c
index 2877782..a24101e 100644
--- a/kernel/sysctl.c
+++ b/kernel/sysctl.c
@@ -1860,6 +1860,15 @@  static int sysrq_sysctl_handler(struct ctl_table *table, int write,
 		.extra1		= &zero,
 		.extra2		= &one_hundred,
 	},
+	{
+		.procname	= "neg-dentry-enforce",
+		.data		= &neg_dentry_enforce,
+		.maxlen		= sizeof(neg_dentry_enforce),
+		.mode		= 0644,
+		.proc_handler	= proc_dointvec_minmax,
+		.extra1		= &zero,
+		.extra2		= &one,
+	},
 	{ }
 };