x86: Hardware breakpoints are not always triggered

From: Andrey Wagin
Date: Thu Jan 28 2016 - 03:31:54 EST


Hi,

We use hardware breakpoints in CRIU and we found that sometimes we set
a break-point, but a process doesn't stop on it.

I write a small reproducer for this bug. It create two processes,
where a parent process traces a child. The parent process sets a
break-point and each time when the child stop on it, the parent sets
the variable "xxx" to A in a child process. The child runs an infinite
loop, where it check the variable "xxx" and sets it to B. If a child
process finds that xxx is equal to B, it exits with a non-zero code,
what means that a break-point was not triggered. The source code is
attached.

The reproducer uses a different break-point address if it is executed
with arguments than when it executed without arguments.

Then I made a few experiments. The bug is triggered, if we execute
this program a few times in a KVM virtual machine.

[root@fc22-vm ptrace]# ( while :; do ./ptrace_breakpoint > /dev/null
|| { echo "FAIL - $?"; break; }; done ) &
[3] 4088
[root@fc22-vm ptrace]# ( while :; do ./ptrace_breakpoint > /dev/null
|| { echo "FAIL - $?"; break; }; done ) &
[4] 4091
[root@fc22-vm ptrace]# ( while :; do ./ptrace_breakpoint 1 2 >
/dev/null || { echo "FAIL - $?"; break; }; done ) &
[5] 4094
[root@fc22-vm ptrace]# ( while :; do ./ptrace_breakpoint 1 2 >
/dev/null || { echo "FAIL - $?"; break; }; done ) &
[6] 4097
[8] 4103
[root@fc22-vm ptrace]# 0087: exit - 5
0131: exited, status=1
0126: wait: No child processes
FAIL - 3

I tried to execute the reproducer on the host (where kvm VM-s are
running), but the bug was not triggered during one hour.

When I executed the reproducer in VM without stopping processes on the
host, I found that a bug is triggered much faster in this case.

[root@fc22-vm ptrace]# ./ptrace_breakpoint 1
....
stop 24675
cont
child2 1
stop 24676
cont
child2 1
child2 5
0088: exit - 5
stop 24677
0132: exited, status=1
cont
0127: wait: No child processes

I know that this bug can be reproduced starting with the 4.2 kernel. I
haven't test older versions of the kernel.

I tried to print drX registers after a break-point. Looks like they
are set correctly.

Maybe someone has any ideas where a problem is or how it can be investigated.

Here is a criu issue for this problem:
https://github.com/xemul/criu/issues/107

Thanks,
Andrew
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/syscall.h> /* For SYS_xxx definitions */

#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <errno.h>
#include <sys/user.h>
#include <asm/debugreg.h>
#include <string.h>

/* Copied from the gdb header gdb/nat/x86-dregs.h */

/* Debug registers' indices. */
#define DR_FIRSTADDR 0
#define DR_LASTADDR 3
#define DR_NADDR 4 /* The number of debug address registers. */
#define DR_STATUS 6 /* Index of debug status register (DR6). */
#define DR_CONTROL 7 /* Index of debug control register (DR7). */

#define DR_LOCAL_ENABLE_SHIFT 0 /* Extra shift to the local enable bit. */
#define DR_GLOBAL_ENABLE_SHIFT 1 /* Extra shift to the global enable bit. */
#define DR_ENABLE_SIZE 2 /* Two enable bits per debug register. */

/* Locally enable the break/watchpoint in the I'th debug register. */
#define X86_DR_LOCAL_ENABLE(i) (1 << (DR_LOCAL_ENABLE_SHIFT + DR_ENABLE_SIZE * (i)))


# define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define pr_perror(fmt, ...) \
fprintf(stderr, "%04d: " fmt ": %s\n", __LINE__, ##__VA_ARGS__, strerror(errno))
#define pr_err(fmt, ...) \
fprintf(stderr, "%04d: " fmt "\n", __LINE__, ##__VA_ARGS__)

int ptrace_set_breakpoint(pid_t pid, void *addr)
{
int ret;

/* Set a breakpoint */
if (ptrace(PTRACE_POKEUSER, pid,
offsetof(struct user, u_debugreg[DR_FIRSTADDR]),
addr)) {
pr_perror("Unable to setup a breakpoint into %d", pid);
return -1;
}

/* Enable the breakpoint */
if (ptrace(PTRACE_POKEUSER, pid,
offsetof(struct user, u_debugreg[DR_CONTROL]),
X86_DR_LOCAL_ENABLE(DR_FIRSTADDR))) {
pr_perror("Unable to enable the breakpoint for %d", pid);
return -1;
}

ret = ptrace(PTRACE_CONT, pid, NULL, NULL);
if (ret) {
pr_perror("Unable to restart the stopped tracee process %d", pid);
return -1;
}

return 1;
}

static long xxx = -1;
int child()
{
printf("child %d\n", xxx);
syscall(__NR_getppid);
if (xxx == 5) {
pr_err("exit - %d", xxx);
exit(1);
}
if (xxx > 0)
xxx = 5;
return 0;
}

int child2()
{
printf("child2 %d\n", xxx);
syscall(__NR_getppid);
if (xxx == 5) {
pr_err("exit - %d", xxx);
exit(1);
}
if (xxx > 0)
xxx = 5;
return 0;
}

int main(int argc, char **argv)
{
pid_t pid;
int status, i = 0;
int (*addr)();

if (argc > 1)
addr = child2;
else
addr = child;

pid = fork();
if (pid == 0) {
while (1)
addr();
}

if (ptrace(PTRACE_ATTACH, pid, NULL, NULL)) {
pr_perror("ptrace");
return 1;
}
if (waitpid(pid, &status, 0) == -1) {
pr_perror("waitpid");
return 1;
}

if (ptrace_set_breakpoint(pid, addr) != 1)
return 2;

while (1) {
if (waitpid(pid, &status, 0) != pid) {
pr_perror("wait");
return 3;
}
printf("stop %d\n", i++);
if (WIFEXITED(status)) {
pr_err("exited, status=%d", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
pr_err("killed by signal %d", WTERMSIG(status));
}
ptrace(PTRACE_POKEDATA, pid, &xxx, 1);
printf("cont\n");
ptrace(PTRACE_CONT, pid, NULL, NULL);
}


return 0;
}