]> git.proxmox.com Git - mirror_lxcfs.git/blob - bindings.c
Merge pull request #342 from brauner/2020-03-04/bugfixes
[mirror_lxcfs.git] / bindings.c
1 /* lxcfs
2 *
3 * Copyright © 2014-2016 Canonical, Inc
4 * Author: Serge Hallyn <serge.hallyn@ubuntu.com>
5 *
6 * See COPYING file for details.
7 */
8
9 #ifndef _GNU_SOURCE
10 #define _GNU_SOURCE
11 #endif
12
13 #ifndef FUSE_USE_VERSION
14 #define FUSE_USE_VERSION 26
15 #endif
16
17 #define _FILE_OFFSET_BITS 64
18
19 #include <dirent.h>
20 #include <errno.h>
21 #include <fcntl.h>
22 #include <fuse.h>
23 #include <inttypes.h>
24 #include <libgen.h>
25 #include <pthread.h>
26 #include <sched.h>
27 #include <stdarg.h>
28 #include <stdbool.h>
29 #include <stdint.h>
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <time.h>
34 #include <unistd.h>
35 #include <wait.h>
36 #include <linux/magic.h>
37 #include <linux/sched.h>
38 #include <sys/epoll.h>
39 #include <sys/mman.h>
40 #include <sys/mount.h>
41 #include <sys/param.h>
42 #include <signal.h>
43 #include <sys/socket.h>
44 #include <sys/syscall.h>
45 #include <sys/sysinfo.h>
46 #include <sys/vfs.h>
47
48 #include "api_extensions.h"
49 #include "bindings.h"
50 #include "config.h"
51 #include "cgroup_fuse.h"
52 #include "cgroups/cgroup.h"
53 #include "cgroups/cgroup_utils.h"
54 #include "memory_utils.h"
55 #include "proc_cpuview.h"
56 #include "utils.h"
57
58 static bool can_use_pidfd;
59
60 /* Define pivot_root() if missing from the C library */
61 #ifndef HAVE_PIVOT_ROOT
62 static int pivot_root(const char *new_root, const char *put_old)
63 {
64 #ifdef __NR_pivot_root
65 return syscall(__NR_pivot_root, new_root, put_old);
66 #else
67 errno = ENOSYS;
68 return -1;
69 #endif
70 }
71 #else
72 extern int pivot_root(const char *new_root, const char *put_old);
73 #endif
74
75 /*
76 * A table caching which pid is init for a pid namespace.
77 * When looking up which pid is init for $qpid, we first
78 * 1. Stat /proc/$qpid/ns/pid.
79 * 2. Check whether the ino_t is in our store.
80 * a. if not, fork a child in qpid's ns to send us
81 * ucred.pid = 1, and read the initpid. Cache
82 * initpid and creation time for /proc/initpid
83 * in a new store entry.
84 * b. if so, verify that /proc/initpid still matches
85 * what we have saved. If not, clear the store
86 * entry and go back to a. If so, return the
87 * cached initpid.
88 */
89 struct pidns_init_store {
90 ino_t ino; /* inode number for /proc/$pid/ns/pid */
91 pid_t initpid; /* the pid of nit in that ns */
92 int init_pidfd;
93 long int ctime; /* the time at which /proc/$initpid was created */
94 struct pidns_init_store *next;
95 long int lastcheck;
96 };
97
98 /* lol - look at how they are allocated in the kernel */
99 #define PIDNS_HASH_SIZE 4096
100 #define HASH(x) ((x) % PIDNS_HASH_SIZE)
101
102 static struct pidns_init_store *pidns_hash_table[PIDNS_HASH_SIZE];
103 static pthread_mutex_t pidns_store_mutex = PTHREAD_MUTEX_INITIALIZER;
104
105 static void lock_mutex(pthread_mutex_t *l)
106 {
107 int ret;
108
109 ret = pthread_mutex_lock(l);
110 if (ret)
111 log_exit("%s - returned %d\n", strerror(ret), ret);
112 }
113
114 struct cgroup_ops *cgroup_ops;
115
116 static void unlock_mutex(pthread_mutex_t *l)
117 {
118 int ret;
119
120 ret = pthread_mutex_unlock(l);
121 if (ret)
122 log_exit("%s - returned %d\n", strerror(ret), ret);
123 }
124
125 static void store_lock(void)
126 {
127 lock_mutex(&pidns_store_mutex);
128 }
129
130 static void store_unlock(void)
131 {
132 unlock_mutex(&pidns_store_mutex);
133 }
134
135 /* /proc/ = 6
136 * +
137 * <pid-as-str> = INTTYPE_TO_STRLEN(pid_t)
138 * +
139 * \0 = 1
140 */
141 #define LXCFS_PROC_PID_LEN \
142 (STRLITERALLEN("/proc/") + INTTYPE_TO_STRLEN(uint64_t) + +1)
143
144 /* Must be called under store_lock */
145 static bool initpid_still_valid(struct pidns_init_store *entry)
146 {
147 bool valid = true;
148
149 if (entry->init_pidfd >= 0) {
150 if (pidfd_send_signal(entry->init_pidfd, 0, NULL, 0))
151 valid = false;
152 } else {
153 struct stat st;
154 char path[LXCFS_PROC_PID_LEN];
155
156 snprintf(path, sizeof(path), "/proc/%d", entry->initpid);
157
158 if (stat(path, &st) || entry->ctime != st.st_ctime)
159 valid = false;
160 }
161
162 return valid;
163 }
164
165 /* Must be called under store_lock */
166 static void remove_initpid(struct pidns_init_store *entry)
167 {
168 struct pidns_init_store *it;
169 int ino_hash;
170
171 lxcfs_debug("Removing cached entry for pid %d from init pid cache",
172 entry->initpid);
173
174 ino_hash = HASH(entry->ino);
175 if (pidns_hash_table[ino_hash] == entry) {
176 pidns_hash_table[ino_hash] = entry->next;
177 close_prot_errno_disarm(entry->init_pidfd);
178 free_disarm(entry);
179 return;
180 }
181
182 it = pidns_hash_table[ino_hash];
183 while (it) {
184 if (it->next == entry) {
185 it->next = entry->next;
186 close_prot_errno_disarm(entry->init_pidfd);
187 free_disarm(entry);
188 return;
189 }
190 it = it->next;
191 }
192 }
193
194 #define PURGE_SECS 5
195 /* Must be called under store_lock */
196 static void prune_initpid_store(void)
197 {
198 static long int last_prune = 0;
199 long int now, threshold;
200
201 if (!last_prune) {
202 last_prune = time(NULL);
203 return;
204 }
205
206 now = time(NULL);
207 if (now < last_prune + PURGE_SECS)
208 return;
209
210 lxcfs_debug("Pruning init pid cache");
211
212 last_prune = now;
213 threshold = now - 2 * PURGE_SECS;
214
215 for (int i = 0; i < PIDNS_HASH_SIZE; i++) {
216 for (struct pidns_init_store *entry = pidns_hash_table[i], *prev = NULL; entry;) {
217 if (entry->lastcheck < threshold) {
218 struct pidns_init_store *cur = entry;
219
220 lxcfs_debug("Removed cache entry for pid %d to init pid cache", cur->initpid);
221
222 if (prev)
223 prev->next = entry->next;
224 else
225 pidns_hash_table[i] = entry->next;
226 entry = entry->next;
227 close_prot_errno_disarm(cur->init_pidfd);
228 free_disarm(cur);
229 } else {
230 prev = entry;
231 entry = entry->next;
232 }
233 }
234 }
235 }
236
237 /* Must be called under store_lock */
238 static void save_initpid(struct stat *sb, pid_t pid)
239 {
240 __do_free struct pidns_init_store *entry = NULL;
241 __do_close_prot_errno int pidfd = -EBADF;
242 char path[LXCFS_PROC_PID_LEN];
243 struct lxcfs_opts *opts = fuse_get_context()->private_data;
244 struct stat st;
245 int ino_hash;
246
247 if (opts && opts->use_pidfd && can_use_pidfd) {
248 pidfd = pidfd_open(pid, 0);
249 if (pidfd < 0)
250 return;
251 }
252
253 snprintf(path, sizeof(path), "/proc/%d", pid);
254 if (stat(path, &st))
255 return;
256
257 entry = malloc(sizeof(*entry));
258 if (entry)
259 return;
260
261 ino_hash = HASH(entry->ino);
262 *entry = (struct pidns_init_store){
263 .ino = sb->st_ino,
264 .initpid = pid,
265 .ctime = st.st_ctime,
266 .next = pidns_hash_table[ino_hash],
267 .lastcheck = time(NULL),
268 .init_pidfd = move_fd(pidfd),
269 };
270 pidns_hash_table[ino_hash] = move_ptr(entry);
271
272 lxcfs_debug("Added cache entry %d for pid %d to init pid cache", ino_hash, pid);
273 }
274
275 /*
276 * Given the stat(2) info for a nsfd pid inode, lookup the init_pid_store
277 * entry for the inode number and creation time. Verify that the init pid
278 * is still valid. If not, remove it. Return the entry if valid, NULL
279 * otherwise.
280 * Must be called under store_lock
281 */
282 static struct pidns_init_store *lookup_verify_initpid(struct stat *sb)
283 {
284 struct pidns_init_store *entry = pidns_hash_table[HASH(sb->st_ino)];
285
286 while (entry) {
287 if (entry->ino == sb->st_ino) {
288 if (initpid_still_valid(entry)) {
289 entry->lastcheck = time(NULL);
290 return entry;
291 }
292
293 remove_initpid(entry);
294 return NULL;
295 }
296 entry = entry->next;
297 }
298
299 return NULL;
300 }
301
302 static int send_creds_clone_wrapper(void *arg)
303 {
304 int sock = PTR_TO_INT(arg);
305 char v = '1'; /* we are the child */
306 struct ucred cred = {
307 .uid = 0,
308 .gid = 0,
309 .pid = 1,
310 };
311
312 return send_creds(sock, &cred, v, true) != SEND_CREDS_OK;
313 }
314
315 /*
316 * Let's use the "standard stack limit" (i.e. glibc thread size default) for
317 * stack sizes: 8MB.
318 */
319 #define __LXCFS_STACK_SIZE (8 * 1024 * 1024)
320 static pid_t lxcfs_clone(int (*fn)(void *), void *arg, int flags)
321 {
322 pid_t ret;
323 void *stack;
324
325 stack = malloc(__LXCFS_STACK_SIZE);
326 if (!stack)
327 return ret_errno(ENOMEM);
328
329 #ifdef __ia64__
330 ret = __clone2(fn, stack, __LXCFS_STACK_SIZE, flags | SIGCHLD, arg, NULL);
331 #else
332 ret = clone(fn, stack + __LXCFS_STACK_SIZE, flags | SIGCHLD, arg, NULL);
333 #endif
334 return ret;
335 }
336
337 #define LXCFS_PROC_PID_NS_LEN \
338 (STRLITERALLEN("/proc/") + INTTYPE_TO_STRLEN(uint64_t) + \
339 STRLITERALLEN("/ns/pid") + 1)
340
341 /*
342 * clone a task which switches to @task's namespace and writes '1'.
343 * over a unix sock so we can read the task's reaper's pid in our
344 * namespace
345 *
346 * Note: glibc's fork() does not respect pidns, which can lead to failed
347 * assertions inside glibc (and thus failed forks) if the child's pid in
348 * the pidns and the parent pid outside are identical. Using clone prevents
349 * this issue.
350 */
351 static void write_task_init_pid_exit(int sock, pid_t target)
352 {
353 __do_close_prot_errno int fd = -EBADF;
354 char path[LXCFS_PROC_PID_NS_LEN];
355 pid_t pid;
356
357 snprintf(path, sizeof(path), "/proc/%d/ns/pid", (int)target);
358 fd = open(path, O_RDONLY | O_CLOEXEC);
359 if (fd < 0)
360 log_exit("write_task_init_pid_exit open of ns/pid");
361
362 if (setns(fd, 0))
363 log_exit("Failed to setns to pid namespace of process %d", target);
364
365 pid = lxcfs_clone(send_creds_clone_wrapper, INT_TO_PTR(sock), 0);
366 if (pid < 0)
367 _exit(EXIT_FAILURE);
368
369 if (pid != 0) {
370 if (!wait_for_pid(pid))
371 _exit(EXIT_FAILURE);
372
373 _exit(EXIT_SUCCESS);
374 }
375 }
376
377 static pid_t get_init_pid_for_task(pid_t task)
378 {
379 char v = '0';
380 pid_t pid_ret = -1;
381 pid_t pid;
382 int sock[2];
383 struct ucred cred;
384
385 if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sock) < 0)
386 return -1;
387
388 pid = fork();
389 if (pid < 0)
390 goto out;
391
392 if (pid == 0) {
393 close(sock[1]);
394 write_task_init_pid_exit(sock[0], task);
395 _exit(EXIT_SUCCESS);
396 }
397
398 if (!recv_creds(sock[1], &cred, &v))
399 goto out;
400
401 pid_ret = cred.pid;
402
403 out:
404 close(sock[0]);
405 close(sock[1]);
406 if (pid > 0)
407 wait_for_pid(pid);
408
409 return pid_ret;
410 }
411
412 pid_t lookup_initpid_in_store(pid_t pid)
413 {
414 pid_t answer = 0;
415 char path[LXCFS_PROC_PID_NS_LEN];
416 struct stat st;
417 struct pidns_init_store *entry;
418
419 snprintf(path, sizeof(path), "/proc/%d/ns/pid", pid);
420
421 store_lock();
422 if (stat(path, &st))
423 goto out;
424
425 entry = lookup_verify_initpid(&st);
426 if (entry) {
427 answer = entry->initpid;
428 goto out;
429 }
430
431 answer = get_init_pid_for_task(pid);
432 if (answer > 0)
433 save_initpid(&st, answer);
434
435 out:
436 /*
437 * Prune at the end in case we're returning the value we were about to
438 * return.
439 */
440 prune_initpid_store();
441
442 store_unlock();
443
444 return answer;
445 }
446
447 /*
448 * Functions needed to setup cgroups in the __constructor__.
449 */
450
451 static bool umount_if_mounted(void)
452 {
453 if (umount2(BASEDIR, MNT_DETACH) < 0 && errno != EINVAL) {
454 lxcfs_error("Failed to unmount %s: %s.\n", BASEDIR, strerror(errno));
455 return false;
456 }
457 return true;
458 }
459
460 /* __typeof__ should be safe to use with all compilers. */
461 typedef __typeof__(((struct statfs *)NULL)->f_type) fs_type_magic;
462 static bool has_fs_type(const struct statfs *fs, fs_type_magic magic_val)
463 {
464 return (fs->f_type == (fs_type_magic)magic_val);
465 }
466
467 /*
468 * looking at fs/proc_namespace.c, it appears we can
469 * actually expect the rootfs entry to very specifically contain
470 * " - rootfs rootfs "
471 * IIUC, so long as we've chrooted so that rootfs is not our root,
472 * the rootfs entry should always be skipped in mountinfo contents.
473 */
474 static bool is_on_ramfs(void)
475 {
476 __do_free char *line = NULL;
477 __do_free void *fopen_cache = NULL;
478 __do_fclose FILE *f = NULL;
479 size_t len = 0;
480
481 f = fopen_cached("/proc/self/mountinfo", "re", &fopen_cache);
482 if (!f)
483 return false;
484
485 while (getline(&line, &len, f) != -1) {
486 int i;
487 char *p, *p2;
488
489 for (p = line, i = 0; p && i < 4; i++)
490 p = strchr(p + 1, ' ');
491 if (!p)
492 continue;
493
494 p2 = strchr(p + 1, ' ');
495 if (!p2)
496 continue;
497 *p2 = '\0';
498 if (strcmp(p + 1, "/") == 0) {
499 /* This is '/'. Is it the ramfs? */
500 p = strchr(p2 + 1, '-');
501 if (p && strncmp(p, "- rootfs rootfs ", 16) == 0)
502 return true;
503 }
504 }
505
506 return false;
507 }
508
509 static int pivot_enter()
510 {
511 __do_close_prot_errno int oldroot = -EBADF, newroot = -EBADF;
512
513 oldroot = open("/", O_DIRECTORY | O_RDONLY);
514 if (oldroot < 0)
515 return log_error_errno(-1, errno,
516 "Failed to open old root for fchdir");
517
518 newroot = open(ROOTDIR, O_DIRECTORY | O_RDONLY);
519 if (newroot < 0)
520 return log_error_errno(-1, errno,
521 "Failed to open new root for fchdir");
522
523 /* change into new root fs */
524 if (fchdir(newroot) < 0)
525 return log_error_errno(-1,
526 errno, "Failed to change directory to new rootfs: %s",
527 ROOTDIR);
528
529 /* pivot_root into our new root fs */
530 if (pivot_root(".", ".") < 0)
531 return log_error_errno(-1, errno,
532 "pivot_root() syscall failed: %s",
533 strerror(errno));
534
535 /*
536 * At this point the old-root is mounted on top of our new-root.
537 * To unmounted it we must not be chdir'd into it, so escape back
538 * to the old-root.
539 */
540 if (fchdir(oldroot) < 0)
541 return log_error_errno(-1, errno, "Failed to enter old root");
542
543 if (umount2(".", MNT_DETACH) < 0)
544 return log_error_errno(-1, errno, "Failed to detach old root");
545
546 if (fchdir(newroot) < 0)
547 return log_error_errno(-1, errno, "Failed to re-enter new root");
548
549 return 0;
550 }
551
552 static int chroot_enter()
553 {
554 if (mount(ROOTDIR, "/", NULL, MS_REC | MS_BIND, NULL)) {
555 lxcfs_error("Failed to recursively bind-mount %s into /.", ROOTDIR);
556 return -1;
557 }
558
559 if (chroot(".") < 0) {
560 lxcfs_error("Call to chroot() failed: %s.\n", strerror(errno));
561 return -1;
562 }
563
564 if (chdir("/") < 0) {
565 lxcfs_error("Failed to change directory: %s.\n", strerror(errno));
566 return -1;
567 }
568
569 return 0;
570 }
571
572 static int permute_and_enter(void)
573 {
574 struct statfs sb;
575
576 if (statfs("/", &sb) < 0) {
577 lxcfs_error("%s\n", "Could not stat / mountpoint.");
578 return -1;
579 }
580
581 /* has_fs_type() is not reliable. When the ramfs is a tmpfs it will
582 * likely report TMPFS_MAGIC. Hence, when it reports no we still check
583 * /proc/1/mountinfo. */
584 if (has_fs_type(&sb, RAMFS_MAGIC) || is_on_ramfs())
585 return chroot_enter();
586
587 if (pivot_enter() < 0) {
588 lxcfs_error("%s\n", "Could not perform pivot root.");
589 return -1;
590 }
591
592 return 0;
593 }
594
595 /* Prepare our new clean root. */
596 static int permute_prepare(void)
597 {
598 if (mkdir(ROOTDIR, 0700) < 0 && errno != EEXIST) {
599 lxcfs_error("%s\n", "Failed to create directory for new root.");
600 return -1;
601 }
602
603 if (mount("/", ROOTDIR, NULL, MS_BIND, 0) < 0) {
604 lxcfs_error("Failed to bind-mount / for new root: %s.\n", strerror(errno));
605 return -1;
606 }
607
608 if (mount(RUNTIME_PATH, ROOTDIR RUNTIME_PATH, NULL, MS_BIND, 0) < 0) {
609 lxcfs_error("Failed to bind-mount /run into new root: %s.\n", strerror(errno));
610 return -1;
611 }
612
613 if (mount(BASEDIR, ROOTDIR BASEDIR, NULL, MS_REC | MS_MOVE, 0) < 0) {
614 printf("Failed to move " BASEDIR " into new root: %s.\n", strerror(errno));
615 return -1;
616 }
617
618 return 0;
619 }
620
621 /* Calls chroot() on ramfs, pivot_root() in all other cases. */
622 static bool permute_root(void)
623 {
624 /* Prepare new root. */
625 if (permute_prepare() < 0)
626 return false;
627
628 /* Pivot into new root. */
629 if (permute_and_enter() < 0)
630 return false;
631
632 return true;
633 }
634
635 static bool cgfs_prepare_mounts(void)
636 {
637 if (!mkdir_p(BASEDIR, 0700)) {
638 lxcfs_error("%s\n", "Failed to create lxcfs cgroup mountpoint.");
639 return false;
640 }
641
642 if (!umount_if_mounted()) {
643 lxcfs_error("%s\n", "Failed to clean up old lxcfs cgroup mountpoint.");
644 return false;
645 }
646
647 if (unshare(CLONE_NEWNS) < 0) {
648 lxcfs_error("Failed to unshare mount namespace: %s.\n", strerror(errno));
649 return false;
650 }
651
652 cgroup_ops->mntns_fd = preserve_ns(getpid(), "mnt");
653 if (cgroup_ops->mntns_fd < 0) {
654 lxcfs_error("Failed to preserve mount namespace: %s.\n", strerror(errno));
655 return false;
656 }
657
658 if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, 0) < 0) {
659 lxcfs_error("Failed to remount / private: %s.\n", strerror(errno));
660 return false;
661 }
662
663 if (mount("tmpfs", BASEDIR, "tmpfs", 0, "size=100000,mode=700") < 0) {
664 lxcfs_error("%s\n", "Failed to mount tmpfs over lxcfs cgroup mountpoint.");
665 return false;
666 }
667
668 return true;
669 }
670
671 static bool cgfs_mount_hierarchies(void)
672 {
673 if (!mkdir_p(BASEDIR DEFAULT_CGROUP_MOUNTPOINT, 0755))
674 return false;
675
676 if (!cgroup_ops->mount(cgroup_ops, BASEDIR))
677 return false;
678
679 for (struct hierarchy **h = cgroup_ops->hierarchies; h && *h; h++) {
680 __do_free char *path = must_make_path(BASEDIR, (*h)->mountpoint, NULL);
681 (*h)->fd = open(path, O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
682 if ((*h)->fd < 0)
683 return false;
684 }
685
686 return true;
687 }
688
689 static bool cgfs_setup_controllers(void)
690 {
691 if (!cgfs_prepare_mounts())
692 return false;
693
694 if (!cgfs_mount_hierarchies())
695 return log_error_errno(false, errno, "Failed to set up private lxcfs cgroup mounts");
696
697 if (!permute_root())
698 return false;
699
700 return true;
701 }
702
703 static void __attribute__((constructor)) lxcfs_init(void)
704 {
705 __do_close_prot_errno int init_ns = -EBADF, pidfd = -EBADF;
706 int i = 0;
707 pid_t pid;
708 char *cret;
709 char cwd[MAXPATHLEN];
710
711 lxcfs_info("Running constructor %s", __func__);
712
713 cgroup_ops = cgroup_init();
714 if (!cgroup_ops)
715 log_exit("Failed to initialize cgroup support");
716
717 /* Preserve initial namespace. */
718 pid = getpid();
719 init_ns = preserve_ns(pid, "mnt");
720 if (init_ns < 0)
721 log_exit("Failed to preserve initial mount namespace");
722
723 cret = getcwd(cwd, MAXPATHLEN);
724 if (!cret)
725 log_exit("%s - Could not retrieve current working directory", strerror(errno));
726
727 /* This function calls unshare(CLONE_NEWNS) our initial mount namespace
728 * to privately mount lxcfs cgroups. */
729 if (!cgfs_setup_controllers())
730 log_exit("Failed to setup private cgroup mounts for lxcfs");
731
732 if (setns(init_ns, 0) < 0)
733 log_exit("%s - Failed to switch back to initial mount namespace", strerror(errno));
734
735 if (!cret || chdir(cwd) < 0)
736 log_exit("%s - Could not change back to original working directory", strerror(errno));
737
738 if (!init_cpuview())
739 log_exit("Failed to init CPU view");
740
741 lxcfs_info("mount namespace: %d", cgroup_ops->mntns_fd);
742 lxcfs_info("hierarchies:");
743
744 for (struct hierarchy **h = cgroup_ops->hierarchies; h && *h; h++, i++) {
745 char **controller_list = (*h)->controllers;
746 __do_free char *controllers = NULL;
747 if (controller_list && *controller_list)
748 controllers = lxc_string_join(",", (const char **)controller_list, false);
749 lxcfs_info(" %2d: fd: %3d: %s", i, (*h)->fd, controllers ?: "");
750 }
751
752 pidfd = pidfd_open(pid, 0);
753 if (pidfd >= 0 && pidfd_send_signal(pidfd, 0, NULL, 0) == 0) {
754 can_use_pidfd = true;
755 lxcfs_info("Kernel supports pidfds");
756 }
757
758 lxcfs_info("api_extensions:");
759 for (i = 0; i < nr_api_extensions; i++)
760 lxcfs_info("- %s", api_extensions[i]);
761 }
762
763 static void __attribute__((destructor)) lxcfs_exit(void)
764 {
765 lxcfs_info("Running destructor %s", __func__);
766
767 free_cpuview();
768 cgroup_exit(cgroup_ops);
769 }