README.md
1# XNU user-space unit-tests
2
3This folder contains unit-tests for in-kernel functionality, build as a user-space process
4
5### Building a test:
6```
7> make -C tests/unit SDKROOT=macosx.internal <test-name>
8```
9This will build XNU as a library and link it into a test executable.
10`<test-name>` is the name of the test executable. There should be a corresponding `<test-name>.c`
11Examples for `<test-name>`: `example_test_osfmk`, `example_test_bsd`
12
13Useful customization for the make command:
14- `VERBOSE=YES` - Show more of the build commands
15- `BUILD_WERROR=0` - When building XNU, Do not convert warnings to errors
16- `SKIP_XNU=1` - Don't try to rebuild XNU
17- `KERNEL_CONFIG=release` - Build XNU in in release rather than 'development'
18- `PRODUCT_CONFIG=...` - Build XNU for a device other than the default. Only macos devices are supported
19- `BUILD_CODE_COVERAGE=1` - Build with coverage support, see section below
20- `FIBERS_PREEMPTION=1` - Build with memory operations instrumentation to simulate preemption, see section below
21- `BUILD_ASAN=1` - Build with AddressSanitizer support
22- `BUILD_UBSAN=1` - Build with UndefinedBehaviourSanitizer support
23- `BUILD_TSAN=1` - Build with ThreadSanitizer support
24
25### Running a test
26The darwintest executable is created in `tests/unit/build/sym/`. To run all tests in an executable:
27```text
28> ./tests/unit/build/sym/<test-name>
29```
30
31### Creating a new test
32- Add a `<test-name>.c` file in this directory with the test code.
33- In the added .c file, add a line that looks like `#define UT_MODULE osfmk`
34This determines the context in which the test is going to be built. This should be
35either "bsd" or "osfmk", depending on where the tested code resides. See example_test_bsd.c, example_test_osfmk.c.
36
37### Building all tests
38To build and run all the unit tests executables do:
39```
40> make -C tests/unit SDKROOT=macosx.internal install
41> ./tests/unit/build/sym/run_unittests.sh
42```
43Another option is to run through the main Makefile:
44```
45> make SDKROOT=macosx.internal xnu_unittests
46> ./BUILD/sym/run_unittests.sh
47```
48This is what the xnu_unittests build alias build. Notice that the output folder is different from the first option.
49
50## Debugging a test
51```
52> xcrun -sdk macosx.internal lldb ./tests/unit/build/sym/<test-name>
53(lldb) run <test-case>
54```
55Notice that if the test executable contains more than one `T_DECL()`s, libdarwintest is going to run each `T_DECL()`
56in a separate child process, so invoking `run` in lldb without the name of a specific `T_DECL()` will debug just the top
57level process and not stop on breakpoints.
58For a better debugging experience wrap debugged code with
59```
60#pragma clang attribute push(__attribute__((noinline, optnone)), apply_to=function)
61...
62#pragma clang attribute pop
63```
64or annotate individual functions with `__attribute__((noinline, optnone))`
65
66The unit-tests Makefile is able to generate files that allow easy debugging experience with various IDEs
67```
68> make SDKROOT=macosx.internal cmds_json
69```
70This make target adds the unit-tests executables that were built since the last `clean` to the `compile_commands.json`
71file at the root of the repository so that IDEs that support this file (VSCode, CLion) know about the tests .c files
72as well as the XNU .c files.
73
74### Debugging with Xcode
75```
76> make SDKROOT=macosx.internal proj_xcode
77```
78This reads the `compile_commands.json` file and generates an Xcode project named `ut_xnu_proj.xcodeproj` with all of
79XNU and the unit-tests source files, and running schemes for the test targets.
80To debug using this project:
81- Start Xcode, open the `ut_xnu_proj.xcodeproj` project
82- At the top bar, select the runnning scheme named after the test executable name (`<test-name>`)
83- In the same menu, press "Edit Scheme", go to "Run"->"Arguments" and add as an argument the name of the `T_DECL()`
84to debug
85- Again at the top bar, to the right of the name of the scheme press `My Mac (arm64e)` to open the Location menu
86- Select `My Mac (arm64)` (instead of `My Mac (arm64e)`)
87- Set a breakpoint in the test
88- Press the Play button at the top bar
89
90### Debugging with VSCode
91```
92> make SDKROOT=macosx.internal proj_vscode
93```
94This reads the `compile_commands.json` file and generates a `.vscode/launch.json` file for VSCode to know about
95the executables to run.
96(if you have such existing file it will be overwritten)
97To debug in VSCode:
98- (one time setup) Install the "LLDB DAP" extension
99 - the "LLDB DAP" extension uses the lldb from the currently selected Xcode.app
100- Open the XNU root folder
101- Press the "Run and Debug" tab at the left bar
102- Select the test executable name from the top menu (`<test-name>`)
103- Press the gear icon next to it to edit launch.json
104- In "args", write the name of the `T_DECL()` to debug
105- Press the green play arrow next to the test name
106
107### Debugging with CLion
108```
109> make SDKROOT=macosx.internal proj_clion
110```
111This reads the `compile_commands.json` file and edits the files in `.idea` for CLion to know about
112the executables to run.
113To debug in CLion you need CLion version 2025.1.3 or above which supports custom external lldb
114- (one time setup) Add a new custom-lldb toolchain:
115 - Open Settings -> "Build, Executaion, Deployment" -> Toolchains
116 - Press the "+" icon above the list
117 - Name the new toolchain "System"
118 - At the bottom, next to "Debugger:" add the path to an installed Xcode.app
119 - (it doesn't have to be the Xcode.app which is currently selected or the one which is used to build XNU)
120- Open the XNU root folder
121- At the top right select the test executable name (`<test-name>`) from the menu
122- Press the menu again "Edit Configurations..."
123- Next to "Program arguments:" write the name of the `T_DECL()` to debug
124- Press the bug icon to at the top right to debug
125
126
127## Running Coverage Analysis
1281. Run the unit-test make command with the coverage option:
129```
130> make -C tests/unit SDKROOT=macosx.internal BUILD_CODE_COVERAGE=1 <test-name>
131```
132This will build XNU, the mocks dylib and the test executable with coverage instrumentation.
1332. Run the unit-test and tell the coverage lib where to save the .profraw file:
134```
135> LLVM_PROFILE_FILE="coverage_data.profraw" ./tests/unit/build/sym/<test-name>
136```
1373. Convert the .profraw file to .profdata file:
138```
139> xcrun -sdk macosx.internal llvm-profdata merge -sparse coverage_data.profraw -o coverage_data.profdata
140```
1414. Generate reports
142
143High-level per-file textual report:
144```
145> xcrun -sdk macosx.internal llvm-cov report ./tests/unit/build/sym/libkernel.development.t6020.dylib -instr-profile=coverage_data.profdata
146```
147Low-level per-line html pages in a directory structure:
148```
149> xcrun -sdk macosx.internal llvm-cov show ./tests/unit/build/sym/libkernel.development.t6020.dylib -instr-profile=coverage_data.profdata --format=html -output-dir ./_cov_html
150> open ./_cov_html/index.html
151```
152Mind that both of these commands take the binary for which we want to show information for, in this case, the XNU dylib.
153If you want to show the coverage for the unit-test executable, put that instead. It's also possible to specify multiple binaries with `-object` argument.
154
155Both these commands can take `-sources` argument followed by the list of source files to limit the source files that would show in the report.
156The names need to be the real paths of the files (relative or absolute), not just the path part that appears in the `report` output.
1575. To check the coverage of a single function add `-name=<func-name>` to the `show` command.
1586. To manually filter out functions from the report, for instance if the source file contains test functions which
159are not interesting for coverage statistics:
160- Add `-show-functions` to the `report` command and redirect the output to a file.
161- From the output, take only the function names with:
162`cat report_output.txt | cut -d " " -f1 | sort | uniq > func_names.txt`
163- Edit the file and remove the functions names that are not needed.
164Mind that in this list, static functions appear with the filename prefixed.
165- Add the prefix `allowlist_fun:` to every line in the file:
166`cat func_names.txt | sed 's/^/allowlist_fun:/' > allow_list.txt`
167- Add the argument `-name-allowlist=allow_list.txt` to the `show` command.
168
169See more documentation:
170https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
171https://llvm.org/docs/CommandGuide/llvm-cov.html
172
173## Deterministic threading with fibers
174The mocks library provides a fibers implementation that can be used by tests including the header files in `mocks/fibers/`.
175
176To access mocks that replace locking and scheduling APIs like lck_mtx_t and waitq functions, the test file must include `mocks/mock_thread.h`
177and use the `UT_USE_FIBERS(1)` macro in the global scope.
178
179By default, the context switch points are placed the entry and exit of the fibers API (e.g. before and after mutex lock) but preemption can be simulated using compiler instrumentation.
180If you add `FIBERS_PREEMPTION=1` to the make command line, every memory load and store in the XNU library and in your test file will be instrumentated to be
181a possible context switch point for the deterministic scheduler.
182
183In addition, a data race detector can be enabled when the test is using fibers with preemption simulation.
184The checker is a probabilistic data race sanitizer based on the [DataCollider](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Erickson.pdf) algorithm and can be used as
185a replacement of ThreadSanitizer (that works with the fibers implementation but there can be false positives) or in combination.
186The checker can be enabled with the macro `UT_FIBERS_USE_CHECKER(1)` in the global scope of the test file or setting the `FIBERS_CHECK_RACES` env var when executing a test with fibers.
187
188For an example test using fibers read `fibers_test`.
189
190
191## FAQ
192- Q: I'm trying to call function X but I get a linker error "Undefined symbols for architecture arm64e: X referenced from..."
193- A: This is likely due to the function being declared as hidden, either using `__private_extern__` at
194the function declaration or a `#pragma GCC visibility push(hidden)`/`#pragma GCC visibility pop` pair around
195where it's defined. You can verify this by doing:
196`nm -m tests/unit/build/obj/libkernel.development.t6020.dylib | grep <function-name>`
197and verifying that the function in questions appears next to a lower-case `t` to mean it's a private symbol
198(as opposed to a capital `T` which means it's exported symbol, or it not appearing at all which means there is
199no such function).
200To fix that, simply change `__private_extern__` to `__exported_hidden` or the `#pragma` pair with
201`__exported_push_hidden`/`__exported_pop`. These keep the visibility the same (hidden) for normal XNU build but
202drop to the default (visible) for the user-mode build.
203
204
205- Q: How to build XNU on-desk if it builds warnings with warnings which are converted to errors?
206- A: In the make command line add `BUILD_WERROR=0`
207
208
209- Q: lldb startup takes a long time and shows many errors about loading symbols
210- A: try doing `dsymForUUID --disable` to disable automaic symbol loading
211