* [PATCH] locking: Warn about state preservation when releasing and re-acquiring locks
@ 2022-09-29 12:11 Elad Lahav
2022-10-06 12:58 ` Paul E. McKenney
0 siblings, 1 reply; 6+ messages in thread
From: Elad Lahav @ 2022-09-29 12:11 UTC (permalink / raw)
To: perfbook; +Cc: Elad Lahav
Signed-off-by: Elad Lahav <e2lahav@gmail.com>
---
CodeSamples/locking/Makefile | 5 +-
CodeSamples/locking/rec_tree_itr.c | 96 ++++++++++++++++++++++++++++++
locking/locking.tex | 32 +++++++++-
3 files changed, 130 insertions(+), 3 deletions(-)
create mode 100644 CodeSamples/locking/rec_tree_itr.c
diff --git a/CodeSamples/locking/Makefile b/CodeSamples/locking/Makefile
index 3663a7d5..cf600636 100644
--- a/CodeSamples/locking/Makefile
+++ b/CodeSamples/locking/Makefile
@@ -1,4 +1,4 @@
-ARCH_INDEPENDENT = locked_list
+ARCH_INDEPENDENT = locked_list rec_tree_itr
ARCH_DEPENDENT = xchglock
PROGS = $(ARCH_INDEPENDENT) $(ARCH_DEPENDENT)
@@ -20,5 +20,8 @@ locked_list: locked_list.c ../api.h
xchglock: xchglock.c ../api.h
cc -g -Wall -o xchglock xchglock.c -lpthread
+rec_tree_itr: rec_tree_itr.c ../api.h
+ cc -g -Wall -o rec_tree_itr rec_tree_itr.c -lpthread
+
clean:
rm -f $(PROGS)
diff --git a/CodeSamples/locking/rec_tree_itr.c b/CodeSamples/locking/rec_tree_itr.c
new file mode 100644
index 00000000..1445668c
--- /dev/null
+++ b/CodeSamples/locking/rec_tree_itr.c
@@ -0,0 +1,96 @@
+/*
+ * locked_list.c: Recursive tree iterator
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, you can access it online at
+ * http://www.gnu.org/licenses/gpl-2.0.html.
+ *
+ * Copyright (c) 2022 Elad Lahav
+ */
+
+#include "../api.h"
+
+#define MAX_CHILDREN 10
+
+struct node {
+ int data;
+ int nchildren;
+ struct node *children[MAX_CHILDREN];
+};
+
+struct tree {
+ spinlock_t s;
+ struct node *root;
+};
+
+//\begin{snippet}[labelbase=ln:locking:rec_tree_iterator:tree_for_each,commandchars=\\\@\$]
+void tree_for_each_rec(struct tree *tr, struct node *nd,
+ void (*callback)(struct node *))
+{
+ spin_unlock(&tr->s);
+ callback(nd);
+ spin_lock(&tr->s);
+
+ for (int i = 0; i < nd->nchildren; i++) {
+ tree_for_each_rec(tr, nd->children[i], callback);
+ }
+}
+
+void tree_for_each(struct tree *tr,
+ void (*callback)(struct node *))
+{
+ spin_lock(&tr->s);
+ tree_for_each_rec(tr, tr->root, callback);
+ spin_unlock(&tr->s);
+}
+//\end{snippet}
+
+void print_node_data(struct node *nd)
+{
+ printf("%d\n", nd->data);
+}
+
+int main(int argc, char *argv[])
+{
+ struct tree tr;
+ struct node *nodes = calloc(sizeof(struct node), 10);
+
+ for (int i = 0; i < 10; i++) {
+ nodes[i].data = 100 + i;
+ }
+
+ spin_lock_init(&tr.s);
+
+ tr.root = &nodes[0];
+
+ nodes[0].nchildren = 3;
+ nodes[0].children[0] = &nodes[1];
+ nodes[0].children[1] = &nodes[2];
+ nodes[0].children[2] = &nodes[3];
+
+ nodes[1].nchildren = 2;
+ nodes[1].children[0] = &nodes[4];
+ nodes[1].children[1] = &nodes[5];
+
+ nodes[2].nchildren = 1;
+ nodes[2].children[0] = &nodes[6];
+
+ nodes[5].nchildren = 3;
+ nodes[5].children[0] = &nodes[7];
+ nodes[5].children[1] = &nodes[8];
+ nodes[5].children[2] = &nodes[9];
+
+ tree_for_each(&tr, print_node_data);
+
+ return 0;
+}
diff --git a/locking/locking.tex b/locking/locking.tex
index 45a12caa..c8343638 100644
--- a/locking/locking.tex
+++ b/locking/locking.tex
@@ -375,6 +375,34 @@ deadlock is avoided if each module separately avoids deadlock.
This rule therefore greatly simplifies deadlock analysis and greatly
improves modularity.
+Nevertheless, the golden rule comes with a warning.
+The state protected by the lock cannot be trusted to survive the release
+and re-acquisition of the lock by the module.
+Such assumptions on state preservation can often be subtle and thus the
+source of many bugs.
+The use of \co{qsort()} in the examples above may not illustrate the
+danger.
+
+Consider, however, the recursive tree iterator in
+\cref{lst:locking:Recursive Tree Iterator}.
+The iterator visits every node in the tree, invoking a user's callback
+function.
+The tree lock is released before the invocation and re-acquired after.
+This code makes dangerous assumptions about the preservation of state,
+such as that the number of children of the current node has not changed,
+that the ancestors stored on the stack by the recursion are still there,
+or even that the visited node itself has not been removed and freed.
+The module author must therefore take great care to ensure either that
+state is preserved (e.g., by acquiring a reference on a node to prevent
+it from being freed) or re-initialized when the lock is acquired after
+the return of the callback function.
+
+\begin{listing}
+\input{CodeSamples/locking/rec_tree_iterator@tree_for_each.fcv}
+\caption{Concurrent List Iterator}
+\label{lst:locking:Recursive Tree Iterator}
+\end{listing}
+
\subsubsection{Layered Locking Hierarchies}
\label{sec:locking:Layered Locking Hierarchies}
@@ -1399,7 +1427,7 @@ for example:
that some FIFO ordering applies for threads of the same priority.
\item Random, so that the new lock holder is chosen randomly from
all threads attempting acquisition, regardless of timing.
-\item
+\item
Unfair, so that a given acquisition might never acquire the lock
(see \cref{sec:locking:Unfairness}).
\end{enumerate}
@@ -1486,7 +1514,7 @@ when switching from read-holder to write-holder mode.
Here are a few possible approaches:
\begin{enumerate}
-\item
+\item
Reader-preference implementations unconditionally favor readers
over writers, possibly allowing write acquisitions to be
indefinitely blocked.
--
2.25.1
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH] locking: Warn about state preservation when releasing and re-acquiring locks
2022-09-29 12:11 [PATCH] locking: Warn about state preservation when releasing and re-acquiring locks Elad Lahav
@ 2022-10-06 12:58 ` Paul E. McKenney
2022-10-06 17:53 ` Elad Lahav
0 siblings, 1 reply; 6+ messages in thread
From: Paul E. McKenney @ 2022-10-06 12:58 UTC (permalink / raw)
To: Elad Lahav; +Cc: perfbook
On Thu, Sep 29, 2022 at 08:11:12AM -0400, Elad Lahav wrote:
> Signed-off-by: Elad Lahav <e2lahav@gmail.com>
Thank you, Elad, and please accept my apologies for being slow.
This is queued and pushed.
I could not resist the urge to wordsmith, so could you please check
the updated patch below?
Also, longer term, would you be willing to add code that makes a
simple but dangerous change in order to better illustrate the problem?
Thanx, Paul
------------------------------------------------------------------------
commit a4e1d87b97f8446dd18db11fd7c5b70633bba69c
Author: Elad Lahav <e2lahav@gmail.com>
Date: Thu Sep 29 08:11:12 2022 -0400
locking: Warn about state preservation when releasing and re-acquiring locks
[ paulmck: Wordsmith. ]
Signed-off-by: Elad Lahav <e2lahav@gmail.com>
Signed-off-by: Paul E. McKenney <paulmck@kernel.org>
diff --git a/CodeSamples/locking/Makefile b/CodeSamples/locking/Makefile
index 3663a7d5..cf600636 100644
--- a/CodeSamples/locking/Makefile
+++ b/CodeSamples/locking/Makefile
@@ -1,4 +1,4 @@
-ARCH_INDEPENDENT = locked_list
+ARCH_INDEPENDENT = locked_list rec_tree_itr
ARCH_DEPENDENT = xchglock
PROGS = $(ARCH_INDEPENDENT) $(ARCH_DEPENDENT)
@@ -20,5 +20,8 @@ locked_list: locked_list.c ../api.h
xchglock: xchglock.c ../api.h
cc -g -Wall -o xchglock xchglock.c -lpthread
+rec_tree_itr: rec_tree_itr.c ../api.h
+ cc -g -Wall -o rec_tree_itr rec_tree_itr.c -lpthread
+
clean:
rm -f $(PROGS)
diff --git a/CodeSamples/locking/rec_tree_itr.c b/CodeSamples/locking/rec_tree_itr.c
new file mode 100644
index 00000000..1445668c
--- /dev/null
+++ b/CodeSamples/locking/rec_tree_itr.c
@@ -0,0 +1,96 @@
+/*
+ * locked_list.c: Recursive tree iterator
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, you can access it online at
+ * http://www.gnu.org/licenses/gpl-2.0.html.
+ *
+ * Copyright (c) 2022 Elad Lahav
+ */
+
+#include "../api.h"
+
+#define MAX_CHILDREN 10
+
+struct node {
+ int data;
+ int nchildren;
+ struct node *children[MAX_CHILDREN];
+};
+
+struct tree {
+ spinlock_t s;
+ struct node *root;
+};
+
+//\begin{snippet}[labelbase=ln:locking:rec_tree_iterator:tree_for_each,commandchars=\\\@\$]
+void tree_for_each_rec(struct tree *tr, struct node *nd,
+ void (*callback)(struct node *))
+{
+ spin_unlock(&tr->s);
+ callback(nd);
+ spin_lock(&tr->s);
+
+ for (int i = 0; i < nd->nchildren; i++) {
+ tree_for_each_rec(tr, nd->children[i], callback);
+ }
+}
+
+void tree_for_each(struct tree *tr,
+ void (*callback)(struct node *))
+{
+ spin_lock(&tr->s);
+ tree_for_each_rec(tr, tr->root, callback);
+ spin_unlock(&tr->s);
+}
+//\end{snippet}
+
+void print_node_data(struct node *nd)
+{
+ printf("%d\n", nd->data);
+}
+
+int main(int argc, char *argv[])
+{
+ struct tree tr;
+ struct node *nodes = calloc(sizeof(struct node), 10);
+
+ for (int i = 0; i < 10; i++) {
+ nodes[i].data = 100 + i;
+ }
+
+ spin_lock_init(&tr.s);
+
+ tr.root = &nodes[0];
+
+ nodes[0].nchildren = 3;
+ nodes[0].children[0] = &nodes[1];
+ nodes[0].children[1] = &nodes[2];
+ nodes[0].children[2] = &nodes[3];
+
+ nodes[1].nchildren = 2;
+ nodes[1].children[0] = &nodes[4];
+ nodes[1].children[1] = &nodes[5];
+
+ nodes[2].nchildren = 1;
+ nodes[2].children[0] = &nodes[6];
+
+ nodes[5].nchildren = 3;
+ nodes[5].children[0] = &nodes[7];
+ nodes[5].children[1] = &nodes[8];
+ nodes[5].children[2] = &nodes[9];
+
+ tree_for_each(&tr, print_node_data);
+
+ return 0;
+}
diff --git a/locking/locking.tex b/locking/locking.tex
index 45a12caa..32f9bad7 100644
--- a/locking/locking.tex
+++ b/locking/locking.tex
@@ -375,6 +375,37 @@ deadlock is avoided if each module separately avoids deadlock.
This rule therefore greatly simplifies deadlock analysis and greatly
improves modularity.
+Nevertheless, this golden rule comes with a warning.
+When you release those locks, any state that they protect is subject
+to arbitrary changes, changes that are all too easy for the function's
+caller to forget, resulting in subtle and difficult-to-reproduce bugs.
+Because the \co{qsort()} comparison function rarely acquires locks,
+let's switch to a different example.
+
+Consider the recursive tree iterator in
+\cref{lst:locking:Recursive Tree Iterator}.
+The iterator visits every node in the tree, invoking a user's callback
+function.
+The tree lock is released before the invocation and re-acquired after return.
+This code makes dangerous assumptions:
+\begin{enumerate*}[(1)]
+\item The number of children of the current node has not changed,
+\item The ancestors stored on the stack by the recursion are still
+ there, and
+\item The visited node itself has not been removed and freed.
+\end{enumerate*}
+One strategy is to ensure that state is preserved despite the lock being
+released, for example, by acquiring a reference on a node to prevent it
+from being freed.
+Alternatively, the state can be re-initialized once the lock is
+re-acquired after the callback function returns.
+
+\begin{listing}
+\input{CodeSamples/locking/rec_tree_iterator@tree_for_each.fcv}
+\caption{Recursive Tree Iterator}
+\label{lst:locking:Recursive Tree Iterator}
+\end{listing}
+
\subsubsection{Layered Locking Hierarchies}
\label{sec:locking:Layered Locking Hierarchies}
@@ -385,10 +416,9 @@ improves modularity.
\label{fig:locking:Layered Locking Hierarchy for qsort()}
\end{figure}
-Unfortunately, it might not be possible for \co{qsort()} to release
-all of its locks before invoking the comparison function.
-In this case, we cannot construct a local locking hierarchy by
-releasing all locks before invoking unknown code.
+Unfortunately, it might be infeasible to preserve state on the one hand
+or to re-initialize it on the other, thus ruling out a local locking
+hierarchy where all locks are released before invoking unknown code.
However, we can instead construct a layered locking hierarchy, as shown in
\cref{fig:locking:Layered Locking Hierarchy for qsort()}.
Here, the \co{cmp()} function uses a new Lock~D that is acquired after
@@ -1399,7 +1429,7 @@ for example:
that some FIFO ordering applies for threads of the same priority.
\item Random, so that the new lock holder is chosen randomly from
all threads attempting acquisition, regardless of timing.
-\item
+\item
Unfair, so that a given acquisition might never acquire the lock
(see \cref{sec:locking:Unfairness}).
\end{enumerate}
@@ -1486,7 +1516,7 @@ when switching from read-holder to write-holder mode.
Here are a few possible approaches:
\begin{enumerate}
-\item
+\item
Reader-preference implementations unconditionally favor readers
over writers, possibly allowing write acquisitions to be
indefinitely blocked.
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH] locking: Warn about state preservation when releasing and re-acquiring locks
2022-10-06 12:58 ` Paul E. McKenney
@ 2022-10-06 17:53 ` Elad Lahav
2022-10-06 18:06 ` Paul E. McKenney
0 siblings, 1 reply; 6+ messages in thread
From: Elad Lahav @ 2022-10-06 17:53 UTC (permalink / raw)
To: paulmck; +Cc: perfbook
New wording looks good to me.
I will come up with an example so dangerous you can stick a tail to it
and call it a wolf.
--Elad
On Thu, 6 Oct 2022 at 08:58, Paul E. McKenney <paulmck@kernel.org> wrote:
>
> On Thu, Sep 29, 2022 at 08:11:12AM -0400, Elad Lahav wrote:
> > Signed-off-by: Elad Lahav <e2lahav@gmail.com>
>
> Thank you, Elad, and please accept my apologies for being slow.
> This is queued and pushed.
>
> I could not resist the urge to wordsmith, so could you please check
> the updated patch below?
>
> Also, longer term, would you be willing to add code that makes a
> simple but dangerous change in order to better illustrate the problem?
>
> Thanx, Paul
>
> ------------------------------------------------------------------------
>
> commit a4e1d87b97f8446dd18db11fd7c5b70633bba69c
> Author: Elad Lahav <e2lahav@gmail.com>
> Date: Thu Sep 29 08:11:12 2022 -0400
>
> locking: Warn about state preservation when releasing and re-acquiring locks
>
> [ paulmck: Wordsmith. ]
>
> Signed-off-by: Elad Lahav <e2lahav@gmail.com>
> Signed-off-by: Paul E. McKenney <paulmck@kernel.org>
>
> diff --git a/CodeSamples/locking/Makefile b/CodeSamples/locking/Makefile
> index 3663a7d5..cf600636 100644
> --- a/CodeSamples/locking/Makefile
> +++ b/CodeSamples/locking/Makefile
> @@ -1,4 +1,4 @@
> -ARCH_INDEPENDENT = locked_list
> +ARCH_INDEPENDENT = locked_list rec_tree_itr
> ARCH_DEPENDENT = xchglock
> PROGS = $(ARCH_INDEPENDENT) $(ARCH_DEPENDENT)
>
> @@ -20,5 +20,8 @@ locked_list: locked_list.c ../api.h
> xchglock: xchglock.c ../api.h
> cc -g -Wall -o xchglock xchglock.c -lpthread
>
> +rec_tree_itr: rec_tree_itr.c ../api.h
> + cc -g -Wall -o rec_tree_itr rec_tree_itr.c -lpthread
> +
> clean:
> rm -f $(PROGS)
> diff --git a/CodeSamples/locking/rec_tree_itr.c b/CodeSamples/locking/rec_tree_itr.c
> new file mode 100644
> index 00000000..1445668c
> --- /dev/null
> +++ b/CodeSamples/locking/rec_tree_itr.c
> @@ -0,0 +1,96 @@
> +/*
> + * locked_list.c: Recursive tree iterator
> + *
> + * This program is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License as published by
> + * the Free Software Foundation; either version 2 of the License, or
> + * (at your option) any later version.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> + * GNU General Public License for more details.
> + *
> + * You should have received a copy of the GNU General Public License
> + * along with this program; if not, you can access it online at
> + * http://www.gnu.org/licenses/gpl-2.0.html.
> + *
> + * Copyright (c) 2022 Elad Lahav
> + */
> +
> +#include "../api.h"
> +
> +#define MAX_CHILDREN 10
> +
> +struct node {
> + int data;
> + int nchildren;
> + struct node *children[MAX_CHILDREN];
> +};
> +
> +struct tree {
> + spinlock_t s;
> + struct node *root;
> +};
> +
> +//\begin{snippet}[labelbase=ln:locking:rec_tree_iterator:tree_for_each,commandchars=\\\@\$]
> +void tree_for_each_rec(struct tree *tr, struct node *nd,
> + void (*callback)(struct node *))
> +{
> + spin_unlock(&tr->s);
> + callback(nd);
> + spin_lock(&tr->s);
> +
> + for (int i = 0; i < nd->nchildren; i++) {
> + tree_for_each_rec(tr, nd->children[i], callback);
> + }
> +}
> +
> +void tree_for_each(struct tree *tr,
> + void (*callback)(struct node *))
> +{
> + spin_lock(&tr->s);
> + tree_for_each_rec(tr, tr->root, callback);
> + spin_unlock(&tr->s);
> +}
> +//\end{snippet}
> +
> +void print_node_data(struct node *nd)
> +{
> + printf("%d\n", nd->data);
> +}
> +
> +int main(int argc, char *argv[])
> +{
> + struct tree tr;
> + struct node *nodes = calloc(sizeof(struct node), 10);
> +
> + for (int i = 0; i < 10; i++) {
> + nodes[i].data = 100 + i;
> + }
> +
> + spin_lock_init(&tr.s);
> +
> + tr.root = &nodes[0];
> +
> + nodes[0].nchildren = 3;
> + nodes[0].children[0] = &nodes[1];
> + nodes[0].children[1] = &nodes[2];
> + nodes[0].children[2] = &nodes[3];
> +
> + nodes[1].nchildren = 2;
> + nodes[1].children[0] = &nodes[4];
> + nodes[1].children[1] = &nodes[5];
> +
> + nodes[2].nchildren = 1;
> + nodes[2].children[0] = &nodes[6];
> +
> + nodes[5].nchildren = 3;
> + nodes[5].children[0] = &nodes[7];
> + nodes[5].children[1] = &nodes[8];
> + nodes[5].children[2] = &nodes[9];
> +
> + tree_for_each(&tr, print_node_data);
> +
> + return 0;
> +}
> diff --git a/locking/locking.tex b/locking/locking.tex
> index 45a12caa..32f9bad7 100644
> --- a/locking/locking.tex
> +++ b/locking/locking.tex
> @@ -375,6 +375,37 @@ deadlock is avoided if each module separately avoids deadlock.
> This rule therefore greatly simplifies deadlock analysis and greatly
> improves modularity.
>
> +Nevertheless, this golden rule comes with a warning.
> +When you release those locks, any state that they protect is subject
> +to arbitrary changes, changes that are all too easy for the function's
> +caller to forget, resulting in subtle and difficult-to-reproduce bugs.
> +Because the \co{qsort()} comparison function rarely acquires locks,
> +let's switch to a different example.
> +
> +Consider the recursive tree iterator in
> +\cref{lst:locking:Recursive Tree Iterator}.
> +The iterator visits every node in the tree, invoking a user's callback
> +function.
> +The tree lock is released before the invocation and re-acquired after return.
> +This code makes dangerous assumptions:
> +\begin{enumerate*}[(1)]
> +\item The number of children of the current node has not changed,
> +\item The ancestors stored on the stack by the recursion are still
> + there, and
> +\item The visited node itself has not been removed and freed.
> +\end{enumerate*}
> +One strategy is to ensure that state is preserved despite the lock being
> +released, for example, by acquiring a reference on a node to prevent it
> +from being freed.
> +Alternatively, the state can be re-initialized once the lock is
> +re-acquired after the callback function returns.
> +
> +\begin{listing}
> +\input{CodeSamples/locking/rec_tree_iterator@tree_for_each.fcv}
> +\caption{Recursive Tree Iterator}
> +\label{lst:locking:Recursive Tree Iterator}
> +\end{listing}
> +
> \subsubsection{Layered Locking Hierarchies}
> \label{sec:locking:Layered Locking Hierarchies}
>
> @@ -385,10 +416,9 @@ improves modularity.
> \label{fig:locking:Layered Locking Hierarchy for qsort()}
> \end{figure}
>
> -Unfortunately, it might not be possible for \co{qsort()} to release
> -all of its locks before invoking the comparison function.
> -In this case, we cannot construct a local locking hierarchy by
> -releasing all locks before invoking unknown code.
> +Unfortunately, it might be infeasible to preserve state on the one hand
> +or to re-initialize it on the other, thus ruling out a local locking
> +hierarchy where all locks are released before invoking unknown code.
> However, we can instead construct a layered locking hierarchy, as shown in
> \cref{fig:locking:Layered Locking Hierarchy for qsort()}.
> Here, the \co{cmp()} function uses a new Lock~D that is acquired after
> @@ -1399,7 +1429,7 @@ for example:
> that some FIFO ordering applies for threads of the same priority.
> \item Random, so that the new lock holder is chosen randomly from
> all threads attempting acquisition, regardless of timing.
> -\item
> +\item
> Unfair, so that a given acquisition might never acquire the lock
> (see \cref{sec:locking:Unfairness}).
> \end{enumerate}
> @@ -1486,7 +1516,7 @@ when switching from read-holder to write-holder mode.
> Here are a few possible approaches:
>
> \begin{enumerate}
> -\item
> +\item
> Reader-preference implementations unconditionally favor readers
> over writers, possibly allowing write acquisitions to be
> indefinitely blocked.
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH] locking: Warn about state preservation when releasing and re-acquiring locks
2022-10-06 17:53 ` Elad Lahav
@ 2022-10-06 18:06 ` Paul E. McKenney
2022-10-16 12:41 ` Elad Lahav
0 siblings, 1 reply; 6+ messages in thread
From: Paul E. McKenney @ 2022-10-06 18:06 UTC (permalink / raw)
To: Elad Lahav; +Cc: perfbook
On Thu, Oct 06, 2022 at 01:53:54PM -0400, Elad Lahav wrote:
> New wording looks good to me.
> I will come up with an example so dangerous you can stick a tail to it
> and call it a wolf.
Now -that- sounds like my kind of example! Thank you!
Thanx, Paul
> --Elad
>
> On Thu, 6 Oct 2022 at 08:58, Paul E. McKenney <paulmck@kernel.org> wrote:
> >
> > On Thu, Sep 29, 2022 at 08:11:12AM -0400, Elad Lahav wrote:
> > > Signed-off-by: Elad Lahav <e2lahav@gmail.com>
> >
> > Thank you, Elad, and please accept my apologies for being slow.
> > This is queued and pushed.
> >
> > I could not resist the urge to wordsmith, so could you please check
> > the updated patch below?
> >
> > Also, longer term, would you be willing to add code that makes a
> > simple but dangerous change in order to better illustrate the problem?
> >
> > Thanx, Paul
> >
> > ------------------------------------------------------------------------
> >
> > commit a4e1d87b97f8446dd18db11fd7c5b70633bba69c
> > Author: Elad Lahav <e2lahav@gmail.com>
> > Date: Thu Sep 29 08:11:12 2022 -0400
> >
> > locking: Warn about state preservation when releasing and re-acquiring locks
> >
> > [ paulmck: Wordsmith. ]
> >
> > Signed-off-by: Elad Lahav <e2lahav@gmail.com>
> > Signed-off-by: Paul E. McKenney <paulmck@kernel.org>
> >
> > diff --git a/CodeSamples/locking/Makefile b/CodeSamples/locking/Makefile
> > index 3663a7d5..cf600636 100644
> > --- a/CodeSamples/locking/Makefile
> > +++ b/CodeSamples/locking/Makefile
> > @@ -1,4 +1,4 @@
> > -ARCH_INDEPENDENT = locked_list
> > +ARCH_INDEPENDENT = locked_list rec_tree_itr
> > ARCH_DEPENDENT = xchglock
> > PROGS = $(ARCH_INDEPENDENT) $(ARCH_DEPENDENT)
> >
> > @@ -20,5 +20,8 @@ locked_list: locked_list.c ../api.h
> > xchglock: xchglock.c ../api.h
> > cc -g -Wall -o xchglock xchglock.c -lpthread
> >
> > +rec_tree_itr: rec_tree_itr.c ../api.h
> > + cc -g -Wall -o rec_tree_itr rec_tree_itr.c -lpthread
> > +
> > clean:
> > rm -f $(PROGS)
> > diff --git a/CodeSamples/locking/rec_tree_itr.c b/CodeSamples/locking/rec_tree_itr.c
> > new file mode 100644
> > index 00000000..1445668c
> > --- /dev/null
> > +++ b/CodeSamples/locking/rec_tree_itr.c
> > @@ -0,0 +1,96 @@
> > +/*
> > + * locked_list.c: Recursive tree iterator
> > + *
> > + * This program is free software; you can redistribute it and/or modify
> > + * it under the terms of the GNU General Public License as published by
> > + * the Free Software Foundation; either version 2 of the License, or
> > + * (at your option) any later version.
> > + *
> > + * This program is distributed in the hope that it will be useful,
> > + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> > + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> > + * GNU General Public License for more details.
> > + *
> > + * You should have received a copy of the GNU General Public License
> > + * along with this program; if not, you can access it online at
> > + * http://www.gnu.org/licenses/gpl-2.0.html.
> > + *
> > + * Copyright (c) 2022 Elad Lahav
> > + */
> > +
> > +#include "../api.h"
> > +
> > +#define MAX_CHILDREN 10
> > +
> > +struct node {
> > + int data;
> > + int nchildren;
> > + struct node *children[MAX_CHILDREN];
> > +};
> > +
> > +struct tree {
> > + spinlock_t s;
> > + struct node *root;
> > +};
> > +
> > +//\begin{snippet}[labelbase=ln:locking:rec_tree_iterator:tree_for_each,commandchars=\\\@\$]
> > +void tree_for_each_rec(struct tree *tr, struct node *nd,
> > + void (*callback)(struct node *))
> > +{
> > + spin_unlock(&tr->s);
> > + callback(nd);
> > + spin_lock(&tr->s);
> > +
> > + for (int i = 0; i < nd->nchildren; i++) {
> > + tree_for_each_rec(tr, nd->children[i], callback);
> > + }
> > +}
> > +
> > +void tree_for_each(struct tree *tr,
> > + void (*callback)(struct node *))
> > +{
> > + spin_lock(&tr->s);
> > + tree_for_each_rec(tr, tr->root, callback);
> > + spin_unlock(&tr->s);
> > +}
> > +//\end{snippet}
> > +
> > +void print_node_data(struct node *nd)
> > +{
> > + printf("%d\n", nd->data);
> > +}
> > +
> > +int main(int argc, char *argv[])
> > +{
> > + struct tree tr;
> > + struct node *nodes = calloc(sizeof(struct node), 10);
> > +
> > + for (int i = 0; i < 10; i++) {
> > + nodes[i].data = 100 + i;
> > + }
> > +
> > + spin_lock_init(&tr.s);
> > +
> > + tr.root = &nodes[0];
> > +
> > + nodes[0].nchildren = 3;
> > + nodes[0].children[0] = &nodes[1];
> > + nodes[0].children[1] = &nodes[2];
> > + nodes[0].children[2] = &nodes[3];
> > +
> > + nodes[1].nchildren = 2;
> > + nodes[1].children[0] = &nodes[4];
> > + nodes[1].children[1] = &nodes[5];
> > +
> > + nodes[2].nchildren = 1;
> > + nodes[2].children[0] = &nodes[6];
> > +
> > + nodes[5].nchildren = 3;
> > + nodes[5].children[0] = &nodes[7];
> > + nodes[5].children[1] = &nodes[8];
> > + nodes[5].children[2] = &nodes[9];
> > +
> > + tree_for_each(&tr, print_node_data);
> > +
> > + return 0;
> > +}
> > diff --git a/locking/locking.tex b/locking/locking.tex
> > index 45a12caa..32f9bad7 100644
> > --- a/locking/locking.tex
> > +++ b/locking/locking.tex
> > @@ -375,6 +375,37 @@ deadlock is avoided if each module separately avoids deadlock.
> > This rule therefore greatly simplifies deadlock analysis and greatly
> > improves modularity.
> >
> > +Nevertheless, this golden rule comes with a warning.
> > +When you release those locks, any state that they protect is subject
> > +to arbitrary changes, changes that are all too easy for the function's
> > +caller to forget, resulting in subtle and difficult-to-reproduce bugs.
> > +Because the \co{qsort()} comparison function rarely acquires locks,
> > +let's switch to a different example.
> > +
> > +Consider the recursive tree iterator in
> > +\cref{lst:locking:Recursive Tree Iterator}.
> > +The iterator visits every node in the tree, invoking a user's callback
> > +function.
> > +The tree lock is released before the invocation and re-acquired after return.
> > +This code makes dangerous assumptions:
> > +\begin{enumerate*}[(1)]
> > +\item The number of children of the current node has not changed,
> > +\item The ancestors stored on the stack by the recursion are still
> > + there, and
> > +\item The visited node itself has not been removed and freed.
> > +\end{enumerate*}
> > +One strategy is to ensure that state is preserved despite the lock being
> > +released, for example, by acquiring a reference on a node to prevent it
> > +from being freed.
> > +Alternatively, the state can be re-initialized once the lock is
> > +re-acquired after the callback function returns.
> > +
> > +\begin{listing}
> > +\input{CodeSamples/locking/rec_tree_iterator@tree_for_each.fcv}
> > +\caption{Recursive Tree Iterator}
> > +\label{lst:locking:Recursive Tree Iterator}
> > +\end{listing}
> > +
> > \subsubsection{Layered Locking Hierarchies}
> > \label{sec:locking:Layered Locking Hierarchies}
> >
> > @@ -385,10 +416,9 @@ improves modularity.
> > \label{fig:locking:Layered Locking Hierarchy for qsort()}
> > \end{figure}
> >
> > -Unfortunately, it might not be possible for \co{qsort()} to release
> > -all of its locks before invoking the comparison function.
> > -In this case, we cannot construct a local locking hierarchy by
> > -releasing all locks before invoking unknown code.
> > +Unfortunately, it might be infeasible to preserve state on the one hand
> > +or to re-initialize it on the other, thus ruling out a local locking
> > +hierarchy where all locks are released before invoking unknown code.
> > However, we can instead construct a layered locking hierarchy, as shown in
> > \cref{fig:locking:Layered Locking Hierarchy for qsort()}.
> > Here, the \co{cmp()} function uses a new Lock~D that is acquired after
> > @@ -1399,7 +1429,7 @@ for example:
> > that some FIFO ordering applies for threads of the same priority.
> > \item Random, so that the new lock holder is chosen randomly from
> > all threads attempting acquisition, regardless of timing.
> > -\item
> > +\item
> > Unfair, so that a given acquisition might never acquire the lock
> > (see \cref{sec:locking:Unfairness}).
> > \end{enumerate}
> > @@ -1486,7 +1516,7 @@ when switching from read-holder to write-holder mode.
> > Here are a few possible approaches:
> >
> > \begin{enumerate}
> > -\item
> > +\item
> > Reader-preference implementations unconditionally favor readers
> > over writers, possibly allowing write acquisitions to be
> > indefinitely blocked.
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH] locking: Warn about state preservation when releasing and re-acquiring locks
2022-10-06 18:06 ` Paul E. McKenney
@ 2022-10-16 12:41 ` Elad Lahav
2022-10-20 23:40 ` Paul E. McKenney
0 siblings, 1 reply; 6+ messages in thread
From: Elad Lahav @ 2022-10-16 12:41 UTC (permalink / raw)
To: paulmck; +Cc: perfbook
>>> Also, longer term, would you be willing to add code that makes a
>>> simple but dangerous change in order to better illustrate the problem?
I sent a patch, but I'm actually not too thrilled with it.
The original code was using nd->children[i], which I believe forces a
compliant compiler to re-read nd->children, avoiding the problem. I was
unable to get GCC 8.3 to cache nd->children, even when making the
iterator and the callback take a pointer to a const node. With the new
version, which uses an explicit iterator, the compiler does store
nd->children in a register that is not reloaded across the loop.
The example I propose can thus fall under the category of "don't do
that". On the other hand, this is just supposed to be a small,
self-contained illustration of the danger.
What do you think?
--Elad
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH] locking: Warn about state preservation when releasing and re-acquiring locks
2022-10-16 12:41 ` Elad Lahav
@ 2022-10-20 23:40 ` Paul E. McKenney
0 siblings, 0 replies; 6+ messages in thread
From: Paul E. McKenney @ 2022-10-20 23:40 UTC (permalink / raw)
To: Elad Lahav; +Cc: perfbook
On Sun, Oct 16, 2022 at 08:41:32AM -0400, Elad Lahav wrote:
> > > > Also, longer term, would you be willing to add code that makes a
> > > > simple but dangerous change in order to better illustrate the problem?
> I sent a patch, but I'm actually not too thrilled with it.
>
> The original code was using nd->children[i], which I believe forces a
> compliant compiler to re-read nd->children, avoiding the problem. I was
> unable to get GCC 8.3 to cache nd->children, even when making the iterator
> and the callback take a pointer to a const node. With the new version, which
> uses an explicit iterator, the compiler does store nd->children in a
> register that is not reloaded across the loop.
>
> The example I propose can thus fall under the category of "don't do that".
> On the other hand, this is just supposed to be a small, self-contained
> illustration of the danger.
>
> What do you think?
I took it as is. Many people would not believe that the pointer is
all that different than the array access.
Thank you!
Thanx, Paul
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2022-10-20 23:40 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-09-29 12:11 [PATCH] locking: Warn about state preservation when releasing and re-acquiring locks Elad Lahav
2022-10-06 12:58 ` Paul E. McKenney
2022-10-06 17:53 ` Elad Lahav
2022-10-06 18:06 ` Paul E. McKenney
2022-10-16 12:41 ` Elad Lahav
2022-10-20 23:40 ` Paul E. McKenney
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.