Skip to content

Commit

Permalink
core: introduce lockdep algorithm
Browse files Browse the repository at this point in the history
This commit introduces an algorithm that may be used to detect improper
usage of locks at runtime. It can detect two kinds errors:

 1. A thread tries to release a lock it does not own,
 2. A thread tries to aquire a lock and the operation could *potentially*
    result in a deadlock.

The potential deadlock detection assumes that the code adheres to a strict
locking hierarchy, in other word, that there is a partial ordering on the
locks so that there can be no situation where circular waits can occur. To
put things simply, any two locks should be acquired in the same order in
the same thread. This addresses the following case:

  [Thread #1]  [Thread #2]

    lock(A)
                 lock(B)
    lock(B)
                 lock(A) <-- deadlock!
      ...

The algorithm builds the lock hierarchy dynamically and reports as soon as
a violation is detected.

The interface is made of two functions: lockdep_lock_acquire() and
lockdep_lock_release(), which are meant to be introduced in the
implementation of the actual lock objects. The "acquire" hook tells the
algorithm that a particular lock is about to be requested by a particular
thread, while the "release" hook is meant to be called before the lock is
actually released. If an error is detected, debugging information is sent
to the console, and panic() is called. The debugging information includes
the lock cycle that was detected (in the above example, {A, B}), as well
as the call stacks at the points where the locks were acquired.

The good thing with such an instrumentation of the locking code is that
there is no need to wait for an actual deadlock to occur in order to
detect potential problems. For instance, the timing of execution in the
above example could be different but the problem would still be detected:

  [Thread #1]  [Thread #2]

    lock(A)
    lock(B)
    unlock(B)
    unlock(A)
                 lock(B)
                 lock(A) <-- error!

A pseudo-TA is added for testing (pta/core_lockdep_tests.c).

This code is based on two sources:
- A presentation called "Dl-Check: dynamic potential deadlock detection
tool for Java programs" [1], although the somewhat complex MNR algorithm
for topological ordering of a DAG was not used;
- A depth-first search algorithm [2] was used instead.

Link: [1] https://www.slideshare.net/IosifItkin/tmpa2017-dlcheck-dynamic-potential-deadlock-detection-tool-for-java-programs
Link: [2] https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
Signed-off-by: Jerome Forissier <[email protected]>
  • Loading branch information
jforissier committed Oct 16, 2018
1 parent ce4fe28 commit 74e4278
Show file tree
Hide file tree
Showing 8 changed files with 692 additions and 7 deletions.
149 changes: 149 additions & 0 deletions core/arch/arm/pta/core_lockdep_tests.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: BSD-2-Clause
/*
* Copyright (c) 2018, Linaro Limited
*/

/*
* Test lockdep with hypothetical thread and lock objects
*/

#include <assert.h>
#include <compiler.h>
#include <kernel/lockdep.h>

#include "core_self_tests.h"

static int __maybe_unused self_test_lockdep1(void)
{
TEE_Result res = TEE_ERROR_GENERIC;
struct lockdep_node_head graph;
struct lockdep_lock_head thread1;
int count = 0;

DMSG("");

TAILQ_INIT(&thread1);
TAILQ_INIT(&graph);

/* Not locked, expect failure */
res = __lockdep_lock_release(&thread1, 1);
if (!res)
return count;
count++;

res = __lockdep_lock_acquire(&graph, &thread1, 1);
if (res)
return count;
count++;

res = __lockdep_lock_release(&thread1, 1);
if (res)
return count;
count++;

res = __lockdep_lock_acquire(&graph, &thread1, 1);
if (res)
return count;
count++;

res = __lockdep_lock_acquire(&graph, &thread1, 3);
if (res)
return count;
count++;

res = __lockdep_lock_acquire(&graph, &thread1, 2);
if (res)
return count;
count++;

res = __lockdep_lock_release(&thread1, 3);
if (res)
return count;
count++;

/* Already locked */
res = __lockdep_lock_acquire(&graph, &thread1, 2);
if (!res)
return count;

lockdep_graph_delete(&graph);
lockdep_queue_delete(&thread1);

return 0;
}

static int __maybe_unused self_test_lockdep2(void)
{
TEE_Result res = TEE_ERROR_GENERIC;
struct lockdep_node_head graph;
struct lockdep_lock_head thread1;
struct lockdep_lock_head thread2;
struct lockdep_lock_head thread3;
int count = 0;

DMSG("");

TAILQ_INIT(&thread1);
TAILQ_INIT(&thread2);
TAILQ_INIT(&thread3);
TAILQ_INIT(&graph);

res = __lockdep_lock_acquire(&graph, &thread1, 1);
if (res)
return count;
count++;

res = __lockdep_lock_acquire(&graph, &thread2, 2);
if (res)
return count;
count++;

res = __lockdep_lock_acquire(&graph, &thread1, 2);
if (res)
return count;
count++;

res = __lockdep_lock_acquire(&graph, &thread3, 3);
if (res)
return count;
count++;

res = __lockdep_lock_acquire(&graph, &thread2, 3);
if (res)
return count;
count++;

/* Deadlock 1-2-3 */
res = __lockdep_lock_acquire(&graph, &thread3, 1);
if (!res)
return count;
count++;

lockdep_graph_delete(&graph);
lockdep_queue_delete(&thread1);
lockdep_queue_delete(&thread2);
lockdep_queue_delete(&thread3);

return 0;
}

TEE_Result core_lockdep_tests(uint32_t nParamTypes __unused,
TEE_Param pParams[TEE_NUM_PARAMS] __unused)

{
int count = 0;

count = self_test_lockdep1();
if (count)
goto out;
count = self_test_lockdep2();
if (count)
goto out;
out:
if (count) {
DMSG("count=%d", count);
return TEE_ERROR_GENERIC;
}

return TEE_SUCCESS;
}
14 changes: 14 additions & 0 deletions core/arch/arm/pta/core_self_tests.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#ifndef CORE_SELF_TESTS_H
#define CORE_SELF_TESTS_H

#include <compiler.h>
#include <tee_api_types.h>
#include <tee_api_defines.h>

Expand All @@ -18,4 +19,17 @@ TEE_Result core_fs_htree_tests(uint32_t nParamTypes,
TEE_Result core_mutex_tests(uint32_t nParamTypes,
TEE_Param pParams[TEE_NUM_PARAMS]);

#ifdef CFG_LOCKDEP
TEE_Result core_lockdep_tests(uint32_t nParamTypes,
TEE_Param pParams[TEE_NUM_PARAMS]);
#else
static inline TEE_Result core_lockdep_tests(
uint32_t nParamTypes __unused,
TEE_Param pParams[TEE_NUM_PARAMS] __unused)
{
return TEE_ERROR_NOT_SUPPORTED;
}

#endif

#endif /*CORE_SELF_TESTS_H*/
2 changes: 2 additions & 0 deletions core/arch/arm/pta/pta_invoke_tests.c
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ static TEE_Result invoke_command(void *pSessionContext __unused,
#endif
case PTA_INVOKE_TESTS_CMD_MUTEX:
return core_mutex_tests(nParamTypes, pParams);
case PTA_INVOKE_TESTS_CMD_LOCKDEP:
return core_lockdep_tests(nParamTypes, pParams);
default:
break;
}
Expand Down
1 change: 1 addition & 0 deletions core/arch/arm/pta/sub.mk
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ srcs-y += core_self_tests.c
srcs-y += interrupt_tests.c
srcs-y += core_mutex_tests.c
srcs-$(CFG_WITH_USER_TA) += core_fs_htree_tests.c
srcs-$(CFG_LOCKDEP) += core_lockdep_tests.c
endif
ifeq ($(CFG_WITH_USER_TA),y)
srcs-$(CFG_SECSTOR_TA_MGMT_PTA) += secstor_ta_mgmt.c
Expand Down
118 changes: 118 additions & 0 deletions core/include/kernel/lockdep.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* Copyright (c) 2018, Linaro Limited
*/

#ifndef __KERNEL_LOCKDEP_H
#define __KERNEL_LOCKDEP_H

#include <compiler.h>
#include <kernel/panic.h>
#include <sys/queue.h>
#include <tee_api_types.h>
#include <trace.h>
#include <types_ext.h>

/*
* Lock graph. If node A has an edge to node B, then A was locked before B in
* the same thread of execution.
*/

struct lockdep_edge {
struct lockdep_node *to;
uintptr_t thread_id;
vaddr_t *acq_stack_from;
vaddr_t *acq_stack_to;
STAILQ_ENTRY(lockdep_edge) link;
};

STAILQ_HEAD(lockdep_edge_head, lockdep_edge);

struct lockdep_node {
uintptr_t lock_id; /* For instance, address of actual lock object */
struct lockdep_edge_head edges;
TAILQ_ENTRY(lockdep_node) link;
uint8_t flags; /* Used temporarily when walking the graph */
};

TAILQ_HEAD(lockdep_node_head, lockdep_node);

/* Per-thread queue of currently owned locks (point to nodes in the graph) */

struct lockdep_lock {
struct lockdep_node *node;
vaddr_t *acq_stack;
TAILQ_ENTRY(lockdep_lock) link;
};

TAILQ_HEAD(lockdep_lock_head, lockdep_lock);

#ifdef CFG_LOCKDEP

/*
* Functions used internally and for testing the algorithm. Actual locking code
* should use the wrappers below (which panic in case of error).
*/
TEE_Result __lockdep_lock_acquire(struct lockdep_node_head *graph,
struct lockdep_lock_head *owned,
uintptr_t id);
TEE_Result __lockdep_lock_release(struct lockdep_lock_head *owned, uintptr_t id);

/* Delete all elements in @graph */
void lockdep_graph_delete(struct lockdep_node_head *graph);

/* Delete all elements in @queue */
void lockdep_queue_delete(struct lockdep_lock_head *queue);

/*
* Acquire lock @id, while already holding the locks in @owned.
* @owned represent the caller; there should be one instance per thread of
* execution. @graph is the directed acyclic graph (DAG) to be used for
* potential deadlock detection; use the same @graph for all the locks of the
* same type as lock @id.
*
* This function will panic() if the acquire operation would result in a lock
* hierarchy violation (potential deadlock).
*/
static inline void lockdep_lock_acquire(struct lockdep_node_head *graph,
struct lockdep_lock_head *owned,
uintptr_t id)
{
TEE_Result res = __lockdep_lock_acquire(graph, owned, id);

if (res) {
EMSG("lockdep: error %#" PRIx32, res);
panic();
}
}

/*
* Release lock @id. The lock is removed from @owned.
*
* This function will panic() if the lock is not held by the caller.
*/
static inline void lockdep_lock_release(struct lockdep_lock_head *owned,
uintptr_t id)
{
TEE_Result res = __lockdep_lock_release(owned, id);

if (res) {
EMSG("lockdep: error %#" PRIx32, res);
panic();
}
}

#else /* CFG_LOCKDEP */

static inline void lockdep_lock_acquire(struct lockdep_node_head *g __unused,
struct lockdep_lock_head *o __unused,
uintptr_t id __unused)
{}

static inline void lockdep_lock_release(struct lockdep_lock_head *o __unused,
uintptr_t id __unused)
{}

#endif /* !CFG_LOCKDEP */

#endif /* !__KERNEL_LOCKDEP_H */
Loading

0 comments on commit 74e4278

Please sign in to comment.