32dad32cad39da291a9eae0e697f489820a30460
[platform/core/test/security-tests.git] / src / security-manager-tests / test_cases_prepare_app.cpp
1 /*
2  * Copyright (c) 2016-2020 Samsung Electronics Co., Ltd. All rights reserved
3  *
4  *    Licensed under the Apache License, Version 2.0 (the "License");
5  *    you may not use this file except in compliance with the License.
6  *    You may obtain a copy of the License at
7  *
8  *        http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *    Unless required by applicable law or agreed to in writing, software
11  *    distributed under the License is distributed on an "AS IS" BASIS,
12  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *    See the License for the specific language governing permissions and
14  *    limitations under the License.
15  */
16
17 #include <poll.h>
18 #include <sys/smack.h>
19 #include <sys/capability.h>
20 #include <sys/prctl.h>
21 #include <sys/eventfd.h>
22
23 #include <cmath>
24 #include <thread>
25 #include <string>
26 #include <memory>
27 #include <mutex>
28 #include <fstream>
29
30 #include <dpl/test/test_runner_child.h>
31 #include <dpl/test/test_runner.h>
32
33 #include <app_install_helper.h>
34 #include <scoped_installer.h>
35 #include <sm_api.h>
36 #include <sm_commons.h>
37 #include <memory.h>
38 #include <tests_common.h>
39
40 using namespace SecurityManagerTest;
41
42 namespace {
43 bool finish = false;
44 const size_t THREADS = 10;
45
46 const std::string APP_TEST_USER = "app_test_user";
47
48 const std::string EXTERNAL_STORAGE_PRIVILEGE = "http://tizen.org/privilege/externalstorage";
49 const std::string MEDIA_STORAGE_PRIVILEGE = "http://tizen.org/privilege/mediastorage";
50
51 const std::string ACCESS_DENIED_DIR_PATH = "/usr/share/security-manager/dummy";
52 const std::string EXTERNAL_STORAGE_DIR_PATH = "/opt/media";
53 const std::string MEDIA_STORAGE_RW_DIR_PATH = "/opt/usr/media";
54 const std::string MEDIA_STORAGE_RO_DIR_PATH = "/opt/usr/home/app_test_user/media";
55
56 typedef std::unique_ptr<_cap_struct, decltype(&cap_free)> CapPtr;
57
58 std::string thread_errors;
59 std::mutex error_mutex;
60
61 #define THREAD_ASSERT_MSG(test, message)                                       \
62     do                                                                         \
63     {                                                                          \
64         if (!(test))                                                           \
65         {                                                                      \
66             std::ostringstream assertMsg;                                      \
67             assertMsg << #test << " " << __FILE__ << " " << __LINE__ << " " << \
68                          message << std::endl;                                 \
69             std::lock_guard<std::mutex> guard(error_mutex);                    \
70             thread_errors.append(assertMsg.str());                             \
71         }                                                                      \
72     } while (0)
73
74 void threadFn(int i, const std::string &expectedLabel)
75 {
76     if (i % 2 == 0) {
77         // block all signals
78         sigset_t sigset;
79         THREAD_ASSERT_MSG(sigfillset(&sigset) == 0, "sigfillset failed");
80         THREAD_ASSERT_MSG(sigprocmask(SIG_BLOCK, &sigset, NULL) == 0, "sigprocmask failed");
81     }
82
83     while (!finish)
84         usleep(1000);
85
86     char* label;
87     THREAD_ASSERT_MSG(smack_new_label_from_self(&label) > 0, "smack_new_label_from_self failed");
88     CStringPtr labelPtr(label);
89
90     THREAD_ASSERT_MSG(expectedLabel.compare(label) == 0,
91                       "Thread " << i << " has a wrong label: " << label);
92
93     CapPtr expectedCaps(cap_init(), cap_free);
94     THREAD_ASSERT_MSG(expectedCaps, "cap_init() failed");
95     THREAD_ASSERT_MSG(cap_clear(expectedCaps.get()) == 0, "cap_clear() failed");
96
97     CapPtr realCaps(cap_get_proc(), cap_free);
98     THREAD_ASSERT_MSG(realCaps, "cap_get_proc() failed");
99     THREAD_ASSERT_MSG(cap_compare(realCaps.get(), expectedCaps.get()) == 0,
100                       "Thread " << i << " has wrong caps");
101 }
102
103 struct ThreadWrapper
104 {
105
106     ThreadWrapper()
107     {
108     }
109     ~ThreadWrapper()
110     {
111         finish = true;
112         thread.join();
113     }
114
115     void run(int i, const std::string &expectedLabel)
116     {
117         THREAD_ASSERT_MSG(!thread.joinable(), "Thread already started");
118         thread = std::thread(threadFn, i, expectedLabel);
119     }
120
121     std::thread thread;
122 };
123
124 ino_t getFileInode(const std::string &path)
125 {
126     struct stat st;
127     if (stat(path.c_str(), &st) != 0)
128         return 0;
129
130     return st.st_ino;
131 }
132
133 std::string getTextFileContents(const std::string &path)
134 {
135     std::ifstream in(path.c_str());
136     if (in.fail())
137         return std::string();
138     std::stringstream ss;
139     ss << in.rdbuf();
140     return ss.str();
141 }
142
143 bool isPathBound(const std::string &what, const std::string &where, pid_t pid = 1)
144 {
145     std::string mountinfoPath = std::string("/proc/") + std::to_string(pid) + "/mountinfo";
146     std::string mountinfo = getTextFileContents(mountinfoPath);
147     std::string line = what + " " + where;
148
149     return std::string::npos != mountinfo.find(line);
150 }
151
152 } // anonymous namespace
153
154 RUNNER_TEST_GROUP_INIT(SECURITY_MANAGER_PREPARE_APP)
155
156 RUNNER_CHILD_TEST(security_manager_100_synchronize_credentials_test)
157 {
158     TemporaryTestUser tmpUser(APP_TEST_USER, GUM_USERTYPE_NORMAL, false);
159     tmpUser.create();
160
161     AppInstallHelper app("app100", tmpUser.getUid());
162     ScopedInstaller appInstall(app);
163     const std::string expectedLabel = app.generateAppLabel();
164
165     pid_t pid = fork();
166     RUNNER_ASSERT_ERRNO_MSG(pid >= 0, "Fork failed");
167     if (pid == 0) {
168         {
169             RUNNER_ASSERT_ERRNO_MSG(setLauncherSecurityAttributes(tmpUser) == 0, "launcher failed");
170             Api::prepareAppCandidate();
171             ThreadWrapper threads[THREADS];
172
173             for (size_t i = 0; i < THREADS; i++)
174                 threads[i].run(i, expectedLabel);
175
176             Api::prepareApp(app.getAppId().c_str());
177         }
178         RUNNER_ASSERT_MSG(thread_errors.empty(), std::endl << thread_errors);
179         exit(0);
180     } else {
181         waitPid(pid);
182         Api::cleanupApp(app.getAppId().c_str(), tmpUser.getUid(), pid);
183     }
184 }
185
186 RUNNER_CHILD_TEST(security_manager_101_create_namespace_test_n)
187 {
188     TemporaryTestUser tmpUser(APP_TEST_USER, GUM_USERTYPE_NORMAL, false);
189     tmpUser.create();
190
191     AppInstallHelper app("app100_n", tmpUser.getUid());
192     ScopedInstaller appInstall(app);
193     const std::string expectedLabel = app.generateAppLabel();
194
195     pid_t pid = fork();
196     RUNNER_ASSERT_ERRNO_MSG(pid >= 0, "Fork failed");
197     if (pid == 0) {
198         {
199             RUNNER_ASSERT_ERRNO_MSG(setLauncherSecurityAttributes(tmpUser) == 0, "launcher failed");
200             ThreadWrapper threads[THREADS];
201
202             for (size_t i = 0; i < THREADS; i++)
203                 threads[i].run(i, expectedLabel);
204
205             Api::prepareAppCandidate(SECURITY_MANAGER_ERROR_INPUT_PARAM);
206         }
207         RUNNER_ASSERT_MSG(!thread_errors.empty(), std::endl << thread_errors);
208         exit(0);
209     } else {
210         waitPid(pid);
211     }
212 }
213
214 RUNNER_CHILD_TEST(security_manager_101_create_namespace_test)
215 {
216     TemporaryTestUser tmpUser(APP_TEST_USER, GUM_USERTYPE_NORMAL, false);
217     tmpUser.create();
218
219     AppInstallHelper app("app101", tmpUser.getUid());
220     ScopedInstaller appInstall(app);
221
222     SynchronizationPipe synchPipe;
223     pid_t pid = fork();
224     RUNNER_ASSERT_ERRNO_MSG(pid >= 0, "Fork failed");
225     if (pid == 0) {
226         synchPipe.claimParentEp();
227         RUNNER_ASSERT_ERRNO_MSG(setLauncherSecurityAttributes(tmpUser) == 0, "launcher failed");
228         Api::prepareAppCandidate();
229         Api::prepareApp(app.getAppId().c_str());
230         synchPipe.post();
231         synchPipe.wait();
232
233         exit(0);
234     } else {
235         synchPipe.claimChildEp();
236         synchPipe.wait();
237
238         std::string appBindPath = std::string("/var/run/user/") + std::to_string(tmpUser.getUid())
239                                   + "/apps/" + app.generateAppLabel() + "/" + std::to_string(pid);
240         std::string appProcPath = std::string("/proc/") + std::to_string(pid) + "/ns/mnt";
241         std::string launcherProcPath = std::string("/proc/") + std::to_string(getpid()) + "/ns/mnt";
242
243         ino_t appBindInode = getFileInode(appBindPath);
244         ino_t appProcInode = getFileInode(appProcPath);
245         ino_t launcherProcInode = getFileInode(launcherProcPath);
246
247         RUNNER_ASSERT_ERRNO_MSG(appBindInode != 0, "get inode failed");
248         RUNNER_ASSERT_ERRNO_MSG(appProcInode != 0, "get inode failed");
249         RUNNER_ASSERT_ERRNO_MSG(launcherProcInode != 0, "get inode failed");
250
251         RUNNER_ASSERT_ERRNO_MSG(launcherProcInode != appProcInode, "create mount namespace failed");
252         RUNNER_ASSERT_ERRNO_MSG(appBindInode == appProcInode, "bind namespace failed");
253
254         synchPipe.post();
255         waitPid(pid);
256         Api::cleanupApp(app.getAppId().c_str(), tmpUser.getUid(), pid);
257     }
258 }
259
260 RUNNER_CHILD_TEST(security_manager_102_check_propagation_test)
261 {
262     TemporaryTestUser tmpUser(APP_TEST_USER, GUM_USERTYPE_NORMAL, false);
263     tmpUser.create();
264
265     AppInstallHelper app("app102", tmpUser.getUid());
266     ScopedInstaller appInstall(app);
267
268     SynchronizationPipe synchPipe;
269     pid_t pid = fork();
270     RUNNER_ASSERT_ERRNO_MSG(pid >= 0, "Fork failed");
271     if (pid == 0) {
272         synchPipe.claimParentEp();
273         RUNNER_ASSERT_ERRNO_MSG(setLauncherSecurityAttributes(tmpUser) == 0, "launcher failed");
274         Api::prepareAppCandidate();
275         Api::prepareApp(app.getAppId().c_str());
276         synchPipe.post();
277         synchPipe.wait();
278
279         exit(0);
280     } else {
281         synchPipe.claimChildEp();
282         synchPipe.wait();
283
284         bool result = isPathBound(ACCESS_DENIED_DIR_PATH, EXTERNAL_STORAGE_DIR_PATH, pid);
285         RUNNER_ASSERT_ERRNO_MSG(result == true, "path is not bound");
286         result = isPathBound(ACCESS_DENIED_DIR_PATH, MEDIA_STORAGE_RW_DIR_PATH, pid);
287         RUNNER_ASSERT_ERRNO_MSG(result == true, "path is not bound");
288         result = isPathBound(ACCESS_DENIED_DIR_PATH, MEDIA_STORAGE_RO_DIR_PATH, pid);
289         RUNNER_ASSERT_ERRNO_MSG(result == true, "path is not bound");
290
291         result = isPathBound(ACCESS_DENIED_DIR_PATH, EXTERNAL_STORAGE_DIR_PATH);
292         RUNNER_ASSERT_ERRNO_MSG(result == false, "path is bound");
293         result = isPathBound(ACCESS_DENIED_DIR_PATH, MEDIA_STORAGE_RW_DIR_PATH);
294         RUNNER_ASSERT_ERRNO_MSG(result == false, "path is bound");
295         result = isPathBound(ACCESS_DENIED_DIR_PATH, MEDIA_STORAGE_RO_DIR_PATH);
296         RUNNER_ASSERT_ERRNO_MSG(result == false, "path is bound");
297
298         synchPipe.post();
299         waitPid(pid);
300         Api::cleanupApp(app.getAppId().c_str(), tmpUser.getUid(), pid);
301     }
302 }
303
304 RUNNER_CHILD_TEST(security_manager_103_policy_change_test)
305 {
306     TemporaryTestUser tmpUser(APP_TEST_USER, GUM_USERTYPE_NORMAL, false);
307     tmpUser.create();
308
309     AppInstallHelper app("app103", tmpUser.getUid());
310     app.addPrivilege(EXTERNAL_STORAGE_PRIVILEGE);
311     app.addPrivilege(MEDIA_STORAGE_PRIVILEGE);
312     ScopedInstaller appInstall(app);
313
314     SynchronizationPipe synchPipe;
315     pid_t pid = fork();
316     RUNNER_ASSERT_ERRNO_MSG(pid >= 0, "Fork failed");
317     if (pid == 0) {
318         synchPipe.claimParentEp();
319         RUNNER_ASSERT_ERRNO_MSG(setLauncherSecurityAttributes(tmpUser) == 0, "launcher failed");
320         Api::prepareAppCandidate();
321         Api::prepareApp(app.getAppId().c_str());
322         synchPipe.post();
323         synchPipe.wait();
324
325         exit(0);
326     } else {
327         synchPipe.claimChildEp();
328         synchPipe.wait();
329
330         bool result = isPathBound(ACCESS_DENIED_DIR_PATH, EXTERNAL_STORAGE_DIR_PATH, pid);
331         RUNNER_ASSERT_ERRNO_MSG(result == false, "path is bound");
332         result = isPathBound(ACCESS_DENIED_DIR_PATH, MEDIA_STORAGE_RW_DIR_PATH, pid);
333         RUNNER_ASSERT_ERRNO_MSG(result == false, "path is bound");
334         result = isPathBound(ACCESS_DENIED_DIR_PATH, MEDIA_STORAGE_RO_DIR_PATH, pid);
335         RUNNER_ASSERT_ERRNO_MSG(result == false, "path is bound");
336
337         PolicyRequest policyRequest;
338         PolicyEntry policyEntry(app.getAppId(), std::to_string(tmpUser.getUid()), EXTERNAL_STORAGE_PRIVILEGE);
339         policyEntry.setLevel(PolicyEntry::LEVEL_DENY);
340         policyRequest.addEntry(policyEntry);
341
342         policyEntry = PolicyEntry(app.getAppId(), std::to_string(tmpUser.getUid()), MEDIA_STORAGE_PRIVILEGE);
343         policyEntry.setLevel(PolicyEntry::LEVEL_DENY);
344         policyRequest.addEntry(policyEntry);
345         Api::sendPolicy(policyRequest);
346
347         result = isPathBound(ACCESS_DENIED_DIR_PATH, EXTERNAL_STORAGE_DIR_PATH, pid);
348         RUNNER_ASSERT_ERRNO_MSG(result == true, "path is not bound");
349         result = isPathBound(ACCESS_DENIED_DIR_PATH, MEDIA_STORAGE_RW_DIR_PATH, pid);
350         RUNNER_ASSERT_ERRNO_MSG(result == true, "path is not bound");
351         result = isPathBound(ACCESS_DENIED_DIR_PATH, MEDIA_STORAGE_RO_DIR_PATH, pid);
352         RUNNER_ASSERT_ERRNO_MSG(result == true, "path is not bound");
353
354         policyEntry = PolicyEntry(app.getAppId(),  std::to_string(tmpUser.getUid()), EXTERNAL_STORAGE_PRIVILEGE);
355         policyEntry.setLevel(PolicyEntry::LEVEL_ALLOW);
356         policyRequest.addEntry(policyEntry);
357
358         policyEntry = PolicyEntry(app.getAppId(),  std::to_string(tmpUser.getUid()), MEDIA_STORAGE_PRIVILEGE);
359         policyEntry.setLevel(PolicyEntry::LEVEL_ALLOW);
360         policyRequest.addEntry(policyEntry);
361         Api::sendPolicy(policyRequest);
362
363         result = isPathBound(ACCESS_DENIED_DIR_PATH, EXTERNAL_STORAGE_DIR_PATH, pid);
364         RUNNER_ASSERT_ERRNO_MSG(result == false, "path is bound");
365         result = isPathBound(ACCESS_DENIED_DIR_PATH, MEDIA_STORAGE_RW_DIR_PATH, pid);
366         RUNNER_ASSERT_ERRNO_MSG(result == false, "path is bound");
367         result = isPathBound(ACCESS_DENIED_DIR_PATH, MEDIA_STORAGE_RO_DIR_PATH, pid);
368         RUNNER_ASSERT_ERRNO_MSG(result == false, "path is bound");
369
370         synchPipe.post();
371         waitPid(pid);
372         Api::cleanupApp(app.getAppId().c_str(), tmpUser.getUid(), pid);
373     }
374 }
375
376 namespace {
377 class Timestamp {
378     uint64_t _;
379     explicit Timestamp(uint64_t ts) : _(ts) {}
380 public:
381     Timestamp operator-(const Timestamp &other) const {
382         RUNNER_ASSERT(_ > other._);
383         return Timestamp(_ - other._);
384     }
385     Timestamp operator+(const Timestamp &other) const {
386         return Timestamp(_ + other._);
387     }
388     bool operator<(const Timestamp &other) const {
389         return _ < other._;
390     }
391     Timestamp() = default;
392     static Timestamp future(uint64_t ns) {
393         timespec ts;
394         const auto res = clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
395         RUNNER_ASSERT_ERRNO(!res);
396         return Timestamp(ts.tv_sec * 1000000000ULL + ts.tv_nsec + ns);
397     }
398     static Timestamp now() {
399         return future(0);
400     }
401     template <size_t nLowDigitsToSkip = 3>
402     static void report(Timestamp *ts, size_t n) {
403         std::sort(ts, ts+n);
404         uint64_t sum = 0, mn = -1, mx = 0;
405         long double qsum = 0;
406         for (size_t i = 0; i < n; i++) {
407             const auto t = ts[i]._;
408             sum += t;
409             qsum += decltype(qsum)(t) * t;
410             mn = std::min(mn, t);
411             mx = std::max(mx, t);
412         }
413         uint64_t avg = sum / n;
414         auto qstddev = qsum/n - decltype(qsum)(avg)*avg;
415         const auto out = [](const char *desc, uint64_t t) {
416             char raw[20];
417         char s[20 + 20/3 + 1];
418         size_t j = 0, i = 0;
419         do
420             raw[j++] = '0' + t % 10ULL;
421         while (t /= 10ULL);
422         for (;;) {
423             s[i++] = raw[--j];
424             if (j <= nLowDigitsToSkip)
425                 break;
426             if (!(j % 3))
427                 s[i++] = ' ';
428         }
429         s[i] = '\0';
430         std::cerr << desc << s;
431     };
432     out("min ", mn);
433     out(" max ", mx);
434     out(" avg ", avg);
435     out(" median ", ts[n/2]._);
436     out(" stddev ", std::floor(std::sqrt(qstddev)));
437     std::cerr << '\n';
438     }
439 };
440
441 template <class T, size_t N>
442 constexpr size_t arraySize(T (&)[N]) { return N; }
443 } // namespace
444
445 RUNNER_TEST(security_manager_200_prepare_app_perf)
446 {
447     constexpr int8_t nThreads = 32;
448     constexpr int8_t nConcurrentAppsSamples[] = { 0 /* 1 app w/ nThreads */, 1, 2, 4, 8, 16, 32 };
449     constexpr uint64_t minTotalBenchTime = 60 * 1000ULL*1000*1000; // 60s
450
451     TemporaryTestUser tmpUser(APP_TEST_USER, GUM_USERTYPE_NORMAL, false);
452     tmpUser.create();
453
454     struct App {
455         AppInstallHelper hlp;
456         pid_t pid;
457     };
458
459     std::vector<Timestamp> candidate, prepare, everything;
460
461     std::vector<App> apps;
462     std::vector<ScopedInstaller> appInstalls;
463
464     constexpr auto nAppsMax = nConcurrentAppsSamples[arraySize(nConcurrentAppsSamples) - 1] ?: 1;
465     apps.reserve(nAppsMax);
466     appInstalls.reserve(nAppsMax);
467
468     const auto uid = tmpUser.getUid();
469     for (int i = 0; i < nAppsMax; i++) {
470         apps.emplace_back(App{AppInstallHelper("app200_" + std::to_string(i), uid), 0});
471         auto &hlp = apps.back().hlp;
472         for (const auto &p : { EXTERNAL_STORAGE_PRIVILEGE, MEDIA_STORAGE_PRIVILEGE,
473                 std::string("http://tizen.org/privilege/camera"),
474                 std::string("http://tizen.org/privilege/internet") })
475             hlp.addPrivilege(p);
476         hlp.createSharedRODir();
477         appInstalls.emplace_back(ScopedInstaller(hlp));
478     }
479
480     for (const auto nConcurrentAppsDesc : nConcurrentAppsSamples) {
481         const auto nConcurrentApps = nConcurrentAppsDesc ?: 1;
482         const auto timeout = Timestamp::future(minTotalBenchTime / arraySize(nConcurrentAppsSamples));
483         do {
484             SynchronizationPipe synchPipe;
485             auto exitEvFd = eventfd(0, 0);
486             RUNNER_ASSERT(exitEvFd >= 0);
487
488             for (int i = 0; i < nConcurrentApps; i++) {
489                 auto &app = apps[i];
490                 const auto pid = fork();
491                 RUNNER_ASSERT_ERRNO_MSG(pid >= 0, "Fork failed");
492                 if (pid)
493                     app.pid = pid;
494                 else {
495                     synchPipe.claimChildEp();
496                     RUNNER_ASSERT_ERRNO_MSG(setLauncherSecurityAttributes(tmpUser) == 0, "launcher failed");
497
498                     const auto appId = app.hlp.getAppId();
499
500                     synchPipe.post(); // declare readiness to start measuring
501                     synchPipe.pollForWait(); // wait for parent to signal all kids simultaneously
502
503                     const auto candBeg = Timestamp::now();
504                     Api::prepareAppCandidate();
505                     const auto candEnd = Timestamp::now();
506
507                     if (!nConcurrentAppsDesc) {
508                         for (int i = 0; i < nThreads; i++)
509                             std::thread([]{ for (;;) usleep(1000); }).detach();
510                     }
511
512                     const auto prepBeg = Timestamp::now();
513                     Api::prepareApp(appId);
514                     const auto prepEnd = Timestamp::now();
515
516                     const Timestamp ts[2] = { candEnd-candBeg, prepEnd-prepBeg };
517                     synchPipe.post(ts, sizeof ts); // post measurement payload
518
519                     // stay idle until all kids are done to simulate idle apps and reduce benchmark noise
520                     pollfd fds[1];
521                     fds->fd = exitEvFd;
522                     fds->events = POLLIN;
523                     auto ret = TEMP_FAILURE_RETRY(poll(fds, 1, -1));
524                     RUNNER_ASSERT_ERRNO(ret > 0);
525
526                     exit(0);
527                 }
528             }
529             synchPipe.claimParentEp();
530
531             for (int i = 0; i < nConcurrentApps; i++) // wait for all kids to be ready to start measurement
532                 synchPipe.wait();
533             synchPipe.post(); // signal all kids to start measuring
534
535             for (int i = 0; i < nConcurrentApps; i++) {
536                 Timestamp ts[2];
537                 synchPipe.wait(ts, sizeof ts);
538                 candidate.emplace_back(ts[0]);
539                 prepare.emplace_back(ts[1]);
540                 everything.emplace_back(ts[0] + ts[1]);
541             }
542
543             RUNNER_ASSERT(!eventfd_write(exitEvFd, 1)); // signal all kids to exit now
544
545             for (int i = 0; i < nConcurrentApps; i++) {
546                 const auto &app = apps[i];
547                 waitPid(app.pid);
548                 Api::cleanupApp(app.hlp.getAppId(), uid, app.pid);
549             }
550         } while (Timestamp::now() < timeout);
551
552         if (!nConcurrentAppsDesc)
553             std::cerr << "additionalThreads " << int(nThreads) << ' ';
554         std::cerr << "nConcurrentApps " << int(nConcurrentApps) << " samples " << candidate.size() << '\n';
555         std::cerr << "  prepareAppCandidate [us]:              ";
556         Timestamp::report(candidate.data(), candidate.size());
557         std::cerr << "  prepareApp [us]:                       ";
558         Timestamp::report(prepare.data(), prepare.size());
559         std::cerr << "  prepareAppCandidate + prepareApp [us]: ";
560         Timestamp::report(everything.data(), everything.size());
561         candidate.clear();
562         prepare.clear();
563         everything.clear();
564     }
565 }