1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
4 This file is part of systemd.
6 Copyright 2015 Lennart Poettering
8 systemd is free software; you can redistribute it and/or modify it
9 under the terms of the GNU Lesser General Public License as published by
10 the Free Software Foundation; either version 2.1 of the License, or
11 (at your option) any later version.
13 systemd is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
18 You should have received a copy of the GNU Lesser General Public License
19 along with systemd; If not, see <http://www.gnu.org/licenses/>.
22 #include <sys/mount.h>
27 #include "path-util.h"
31 #include "cgroup-util.h"
33 #include "nspawn-mount.h"
35 CustomMount
* custom_mount_add(CustomMount
**l
, unsigned *n
, CustomMountType t
) {
41 assert(t
< _CUSTOM_MOUNT_TYPE_MAX
);
43 c
= realloc(*l
, (*n
+ 1) * sizeof(CustomMount
));
51 *ret
= (CustomMount
) { .type
= t
};
56 void custom_mount_free_all(CustomMount
*l
, unsigned n
) {
59 for (i
= 0; i
< n
; i
++) {
60 CustomMount
*m
= l
+ i
;
67 (void) rm_rf(m
->work_dir
, REMOVE_ROOT
|REMOVE_PHYSICAL
);
77 int custom_mount_compare(const void *a
, const void *b
) {
78 const CustomMount
*x
= a
, *y
= b
;
81 r
= path_compare(x
->destination
, y
->destination
);
85 if (x
->type
< y
->type
)
87 if (x
->type
> y
->type
)
93 int bind_mount_parse(CustomMount
**l
, unsigned *n
, const char *s
, bool read_only
) {
94 _cleanup_free_
char *source
= NULL
, *destination
= NULL
, *opts
= NULL
;
102 r
= extract_many_words(&p
, ":", EXTRACT_DONT_COALESCE_SEPARATORS
, &source
, &destination
, NULL
);
109 destination
= strdup(source
);
114 if (r
== 2 && !isempty(p
)) {
120 if (!path_is_absolute(source
))
123 if (!path_is_absolute(destination
))
126 m
= custom_mount_add(l
, n
, CUSTOM_MOUNT_BIND
);
131 m
->destination
= destination
;
132 m
->read_only
= read_only
;
135 source
= destination
= opts
= NULL
;
139 int tmpfs_mount_parse(CustomMount
**l
, unsigned *n
, const char *s
) {
140 _cleanup_free_
char *path
= NULL
, *opts
= NULL
;
149 r
= extract_first_word(&p
, &path
, ":", EXTRACT_DONT_COALESCE_SEPARATORS
);
156 opts
= strdup("mode=0755");
162 if (!path_is_absolute(path
))
165 m
= custom_mount_add(l
, n
, CUSTOM_MOUNT_TMPFS
);
169 m
->destination
= path
;
176 static int tmpfs_patch_options(
178 bool userns
, uid_t uid_shift
, uid_t uid_range
,
179 const char *selinux_apifs_context
,
184 if (userns
&& uid_shift
!= 0) {
185 assert(uid_shift
!= UID_INVALID
);
188 (void) asprintf(&buf
, "%s,uid=" UID_FMT
",gid=" UID_FMT
, options
, uid_shift
, uid_shift
);
190 (void) asprintf(&buf
, "uid=" UID_FMT
",gid=" UID_FMT
, uid_shift
, uid_shift
);
198 if (selinux_apifs_context
) {
202 t
= strjoin(options
, ",context=\"", selinux_apifs_context
, "\"", NULL
);
204 t
= strjoin("context=\"", selinux_apifs_context
, "\"", NULL
);
219 int mount_sysfs(const char *dest
) {
220 const char *full
, *top
, *x
;
222 top
= prefix_roota(dest
, "/sys");
223 full
= prefix_roota(top
, "/full");
225 (void) mkdir(full
, 0755);
227 if (mount("sysfs", full
, "sysfs", MS_RDONLY
|MS_NOSUID
|MS_NOEXEC
|MS_NODEV
, NULL
) < 0)
228 return log_error_errno(errno
, "Failed to mount sysfs to %s: %m", full
);
230 FOREACH_STRING(x
, "block", "bus", "class", "dev", "devices", "kernel") {
231 _cleanup_free_
char *from
= NULL
, *to
= NULL
;
233 from
= prefix_root(full
, x
);
237 to
= prefix_root(top
, x
);
241 (void) mkdir(to
, 0755);
243 if (mount(from
, to
, NULL
, MS_BIND
, NULL
) < 0)
244 return log_error_errno(errno
, "Failed to mount /sys/%s into place: %m", x
);
246 if (mount(NULL
, to
, NULL
, MS_BIND
|MS_RDONLY
|MS_NOSUID
|MS_NOEXEC
|MS_NODEV
|MS_REMOUNT
, NULL
) < 0)
247 return log_error_errno(errno
, "Failed to mount /sys/%s read-only: %m", x
);
250 if (umount(full
) < 0)
251 return log_error_errno(errno
, "Failed to unmount %s: %m", full
);
254 return log_error_errno(errno
, "Failed to remove %s: %m", full
);
256 x
= prefix_roota(top
, "/fs/kdbus");
257 (void) mkdir(x
, 0755);
259 if (mount(NULL
, top
, NULL
, MS_BIND
|MS_RDONLY
|MS_NOSUID
|MS_NOEXEC
|MS_NODEV
|MS_REMOUNT
, NULL
) < 0)
260 return log_error_errno(errno
, "Failed to make %s read-only: %m", top
);
265 int mount_all(const char *dest
,
266 bool use_userns
, bool in_userns
,
267 uid_t uid_shift
, uid_t uid_range
,
268 const char *selinux_apifs_context
) {
270 typedef struct MountPoint
{
280 static const MountPoint mount_table
[] = {
281 { "proc", "/proc", "proc", NULL
, MS_NOSUID
|MS_NOEXEC
|MS_NODEV
, true, true },
282 { "/proc/sys", "/proc/sys", NULL
, NULL
, MS_BIND
, true, true }, /* Bind mount first */
283 { NULL
, "/proc/sys", NULL
, NULL
, MS_BIND
|MS_RDONLY
|MS_NOSUID
|MS_NOEXEC
|MS_NODEV
|MS_REMOUNT
, true, true }, /* Then, make it r/o */
284 { "tmpfs", "/sys", "tmpfs", "mode=755", MS_NOSUID
|MS_NOEXEC
|MS_NODEV
, true, false },
285 { "tmpfs", "/dev", "tmpfs", "mode=755", MS_NOSUID
|MS_STRICTATIME
, true, false },
286 { "tmpfs", "/dev/shm", "tmpfs", "mode=1777", MS_NOSUID
|MS_NODEV
|MS_STRICTATIME
, true, false },
287 { "tmpfs", "/run", "tmpfs", "mode=755", MS_NOSUID
|MS_NODEV
|MS_STRICTATIME
, true, false },
288 { "tmpfs", "/tmp", "tmpfs", "mode=1777", MS_STRICTATIME
, true, false },
290 { "/sys/fs/selinux", "/sys/fs/selinux", NULL
, NULL
, MS_BIND
, false, false }, /* Bind mount first */
291 { NULL
, "/sys/fs/selinux", NULL
, NULL
, MS_BIND
|MS_RDONLY
|MS_NOSUID
|MS_NOEXEC
|MS_NODEV
|MS_REMOUNT
, false, false }, /* Then, make it r/o */
298 for (k
= 0; k
< ELEMENTSOF(mount_table
); k
++) {
299 _cleanup_free_
char *where
= NULL
, *options
= NULL
;
302 if (in_userns
!= mount_table
[k
].userns
)
305 where
= prefix_root(dest
, mount_table
[k
].where
);
309 r
= path_is_mount_point(where
, AT_SYMLINK_FOLLOW
);
310 if (r
< 0 && r
!= -ENOENT
)
311 return log_error_errno(r
, "Failed to detect whether %s is a mount point: %m", where
);
313 /* Skip this entry if it is not a remount. */
314 if (mount_table
[k
].what
&& r
> 0)
317 r
= mkdir_p(where
, 0755);
319 if (mount_table
[k
].fatal
)
320 return log_error_errno(r
, "Failed to create directory %s: %m", where
);
322 log_warning_errno(r
, "Failed to create directory %s: %m", where
);
326 o
= mount_table
[k
].options
;
327 if (streq_ptr(mount_table
[k
].type
, "tmpfs")) {
328 r
= tmpfs_patch_options(o
, use_userns
, uid_shift
, uid_range
, selinux_apifs_context
, &options
);
335 if (mount(mount_table
[k
].what
,
338 mount_table
[k
].flags
,
341 if (mount_table
[k
].fatal
)
342 return log_error_errno(errno
, "mount(%s) failed: %m", where
);
344 log_warning_errno(errno
, "mount(%s) failed, ignoring: %m", where
);
351 static int parse_mount_bind_options(const char *options
, unsigned long *mount_flags
, char **mount_opts
) {
352 const char *p
= options
;
353 unsigned long flags
= *mount_flags
;
359 _cleanup_free_
char *word
= NULL
;
360 int r
= extract_first_word(&p
, &word
, ",", 0);
362 return log_error_errno(r
, "Failed to extract mount option: %m");
366 if (streq(word
, "rbind"))
368 else if (streq(word
, "norbind"))
371 log_error("Invalid bind mount option: %s", word
);
376 *mount_flags
= flags
;
377 /* in the future mount_opts will hold string options for mount(2) */
383 static int mount_bind(const char *dest
, CustomMount
*m
) {
384 struct stat source_st
, dest_st
;
386 unsigned long mount_flags
= MS_BIND
| MS_REC
;
387 _cleanup_free_
char *mount_opts
= NULL
;
393 r
= parse_mount_bind_options(m
->options
, &mount_flags
, &mount_opts
);
398 if (stat(m
->source
, &source_st
) < 0)
399 return log_error_errno(errno
, "Failed to stat %s: %m", m
->source
);
401 where
= prefix_roota(dest
, m
->destination
);
403 if (stat(where
, &dest_st
) >= 0) {
404 if (S_ISDIR(source_st
.st_mode
) && !S_ISDIR(dest_st
.st_mode
)) {
405 log_error("Cannot bind mount directory %s on file %s.", m
->source
, where
);
409 if (!S_ISDIR(source_st
.st_mode
) && S_ISDIR(dest_st
.st_mode
)) {
410 log_error("Cannot bind mount file %s on directory %s.", m
->source
, where
);
414 } else if (errno
== ENOENT
) {
415 r
= mkdir_parents_label(where
, 0755);
417 return log_error_errno(r
, "Failed to make parents of %s: %m", where
);
419 log_error_errno(errno
, "Failed to stat %s: %m", where
);
423 /* Create the mount point. Any non-directory file can be
424 * mounted on any non-directory file (regular, fifo, socket,
427 if (S_ISDIR(source_st
.st_mode
))
428 r
= mkdir_label(where
, 0755);
431 if (r
< 0 && r
!= -EEXIST
)
432 return log_error_errno(r
, "Failed to create mount point %s: %m", where
);
434 if (mount(m
->source
, where
, NULL
, mount_flags
, mount_opts
) < 0)
435 return log_error_errno(errno
, "mount(%s) failed: %m", where
);
438 r
= bind_remount_recursive(where
, true);
440 return log_error_errno(r
, "Read-only bind mount failed: %m");
446 static int mount_tmpfs(
449 bool userns
, uid_t uid_shift
, uid_t uid_range
,
450 const char *selinux_apifs_context
) {
452 const char *where
, *options
;
453 _cleanup_free_
char *buf
= NULL
;
459 where
= prefix_roota(dest
, m
->destination
);
461 r
= mkdir_p_label(where
, 0755);
462 if (r
< 0 && r
!= -EEXIST
)
463 return log_error_errno(r
, "Creating mount point for tmpfs %s failed: %m", where
);
465 r
= tmpfs_patch_options(m
->options
, userns
, uid_shift
, uid_range
, selinux_apifs_context
, &buf
);
468 options
= r
> 0 ? buf
: m
->options
;
470 if (mount("tmpfs", where
, "tmpfs", MS_NODEV
|MS_STRICTATIME
, options
) < 0)
471 return log_error_errno(errno
, "tmpfs mount to %s failed: %m", where
);
476 static char *joined_and_escaped_lower_dirs(char * const *lower
) {
477 _cleanup_strv_free_
char **sv
= NULL
;
479 sv
= strv_copy(lower
);
485 if (!strv_shell_escape(sv
, ",:"))
488 return strv_join(sv
, ":");
491 static int mount_overlay(const char *dest
, CustomMount
*m
) {
492 _cleanup_free_
char *lower
= NULL
;
493 const char *where
, *options
;
499 where
= prefix_roota(dest
, m
->destination
);
501 r
= mkdir_label(where
, 0755);
502 if (r
< 0 && r
!= -EEXIST
)
503 return log_error_errno(r
, "Creating mount point for overlay %s failed: %m", where
);
505 (void) mkdir_p_label(m
->source
, 0755);
507 lower
= joined_and_escaped_lower_dirs(m
->lower
);
512 _cleanup_free_
char *escaped_source
= NULL
;
514 escaped_source
= shell_escape(m
->source
, ",:");
518 options
= strjoina("lowerdir=", escaped_source
, ":", lower
);
520 _cleanup_free_
char *escaped_source
= NULL
, *escaped_work_dir
= NULL
;
523 (void) mkdir_label(m
->work_dir
, 0700);
525 escaped_source
= shell_escape(m
->source
, ",:");
528 escaped_work_dir
= shell_escape(m
->work_dir
, ",:");
529 if (!escaped_work_dir
)
532 options
= strjoina("lowerdir=", lower
, ",upperdir=", escaped_source
, ",workdir=", escaped_work_dir
);
535 if (mount("overlay", where
, "overlay", m
->read_only
? MS_RDONLY
: 0, options
) < 0)
536 return log_error_errno(errno
, "overlay mount to %s failed: %m", where
);
543 CustomMount
*mounts
, unsigned n
,
544 bool userns
, uid_t uid_shift
, uid_t uid_range
,
545 const char *selinux_apifs_context
) {
552 for (i
= 0; i
< n
; i
++) {
553 CustomMount
*m
= mounts
+ i
;
557 case CUSTOM_MOUNT_BIND
:
558 r
= mount_bind(dest
, m
);
561 case CUSTOM_MOUNT_TMPFS
:
562 r
= mount_tmpfs(dest
, m
, userns
, uid_shift
, uid_range
, selinux_apifs_context
);
565 case CUSTOM_MOUNT_OVERLAY
:
566 r
= mount_overlay(dest
, m
);
570 assert_not_reached("Unknown custom mount type");
580 static int mount_legacy_cgroup_hierarchy(const char *dest
, const char *controller
, const char *hierarchy
, bool read_only
) {
584 to
= strjoina(strempty(dest
), "/sys/fs/cgroup/", hierarchy
);
586 r
= path_is_mount_point(to
, 0);
587 if (r
< 0 && r
!= -ENOENT
)
588 return log_error_errno(r
, "Failed to determine if %s is mounted already: %m", to
);
594 /* The superblock mount options of the mount point need to be
595 * identical to the hosts', and hence writable... */
596 if (mount("cgroup", to
, "cgroup", MS_NOSUID
|MS_NOEXEC
|MS_NODEV
, controller
) < 0)
597 return log_error_errno(errno
, "Failed to mount to %s: %m", to
);
599 /* ... hence let's only make the bind mount read-only, not the
602 if (mount(NULL
, to
, NULL
, MS_BIND
|MS_REMOUNT
|MS_NOSUID
|MS_NOEXEC
|MS_NODEV
|MS_RDONLY
, NULL
) < 0)
603 return log_error_errno(errno
, "Failed to remount %s read-only: %m", to
);
608 static int mount_legacy_cgroups(
610 bool userns
, uid_t uid_shift
, uid_t uid_range
,
611 const char *selinux_apifs_context
) {
613 _cleanup_set_free_free_ Set
*controllers
= NULL
;
614 const char *cgroup_root
;
617 cgroup_root
= prefix_roota(dest
, "/sys/fs/cgroup");
619 (void) mkdir_p(cgroup_root
, 0755);
621 /* Mount a tmpfs to /sys/fs/cgroup if it's not mounted there yet. */
622 r
= path_is_mount_point(cgroup_root
, AT_SYMLINK_FOLLOW
);
624 return log_error_errno(r
, "Failed to determine if /sys/fs/cgroup is already mounted: %m");
626 _cleanup_free_
char *options
= NULL
;
628 r
= tmpfs_patch_options("mode=755", userns
, uid_shift
, uid_range
, selinux_apifs_context
, &options
);
632 if (mount("tmpfs", cgroup_root
, "tmpfs", MS_NOSUID
|MS_NOEXEC
|MS_NODEV
|MS_STRICTATIME
, options
) < 0)
633 return log_error_errno(errno
, "Failed to mount /sys/fs/cgroup: %m");
636 if (cg_unified() > 0)
637 goto skip_controllers
;
639 controllers
= set_new(&string_hash_ops
);
643 r
= cg_kernel_controllers(controllers
);
645 return log_error_errno(r
, "Failed to determine cgroup controllers: %m");
648 _cleanup_free_
char *controller
= NULL
, *origin
= NULL
, *combined
= NULL
;
650 controller
= set_steal_first(controllers
);
654 origin
= prefix_root("/sys/fs/cgroup/", controller
);
658 r
= readlink_malloc(origin
, &combined
);
660 /* Not a symbolic link, but directly a single cgroup hierarchy */
662 r
= mount_legacy_cgroup_hierarchy(dest
, controller
, controller
, true);
667 return log_error_errno(r
, "Failed to read link %s: %m", origin
);
669 _cleanup_free_
char *target
= NULL
;
671 target
= prefix_root(dest
, origin
);
675 /* A symbolic link, a combination of controllers in one hierarchy */
677 if (!filename_is_valid(combined
)) {
678 log_warning("Ignoring invalid combined hierarchy %s.", combined
);
682 r
= mount_legacy_cgroup_hierarchy(dest
, combined
, combined
, true);
686 r
= symlink_idempotent(combined
, target
);
688 log_error("Invalid existing symlink for combined hierarchy");
692 return log_error_errno(r
, "Failed to create symlink for combined hierarchy: %m");
697 r
= mount_legacy_cgroup_hierarchy(dest
, "none,name=systemd,xattr", "systemd", false);
701 if (mount(NULL
, cgroup_root
, NULL
, MS_REMOUNT
|MS_NOSUID
|MS_NOEXEC
|MS_NODEV
|MS_STRICTATIME
|MS_RDONLY
, "mode=755") < 0)
702 return log_error_errno(errno
, "Failed to remount %s read-only: %m", cgroup_root
);
707 static int mount_unified_cgroups(const char *dest
) {
713 p
= prefix_roota(dest
, "/sys/fs/cgroup");
715 (void) mkdir_p(p
, 0755);
717 r
= path_is_mount_point(p
, AT_SYMLINK_FOLLOW
);
719 return log_error_errno(r
, "Failed to determine if %s is mounted already: %m", p
);
721 p
= prefix_roota(dest
, "/sys/fs/cgroup/cgroup.procs");
722 if (access(p
, F_OK
) >= 0)
725 return log_error_errno(errno
, "Failed to determine if mount point %s contains the unified cgroup hierarchy: %m", p
);
727 log_error("%s is already mounted but not a unified cgroup hierarchy. Refusing.", p
);
731 if (mount("cgroup", p
, "cgroup", MS_NOSUID
|MS_NOEXEC
|MS_NODEV
, "__DEVEL__sane_behavior") < 0)
732 return log_error_errno(errno
, "Failed to mount unified cgroup hierarchy to %s: %m", p
);
739 bool unified_requested
,
740 bool userns
, uid_t uid_shift
, uid_t uid_range
,
741 const char *selinux_apifs_context
) {
743 if (unified_requested
)
744 return mount_unified_cgroups(dest
);
746 return mount_legacy_cgroups(dest
, userns
, uid_shift
, uid_range
, selinux_apifs_context
);
749 int mount_systemd_cgroup_writable(
751 bool unified_requested
) {
753 _cleanup_free_
char *own_cgroup_path
= NULL
;
754 const char *systemd_root
, *systemd_own
;
759 r
= cg_pid_get_path(NULL
, 0, &own_cgroup_path
);
761 return log_error_errno(r
, "Failed to determine our own cgroup path: %m");
763 /* If we are living in the top-level, then there's nothing to do... */
764 if (path_equal(own_cgroup_path
, "/"))
767 if (unified_requested
) {
768 systemd_own
= strjoina(dest
, "/sys/fs/cgroup", own_cgroup_path
);
769 systemd_root
= prefix_roota(dest
, "/sys/fs/cgroup");
771 systemd_own
= strjoina(dest
, "/sys/fs/cgroup/systemd", own_cgroup_path
);
772 systemd_root
= prefix_roota(dest
, "/sys/fs/cgroup/systemd");
775 /* Make our own cgroup a (writable) bind mount */
776 if (mount(systemd_own
, systemd_own
, NULL
, MS_BIND
, NULL
) < 0)
777 return log_error_errno(errno
, "Failed to turn %s into a bind mount: %m", own_cgroup_path
);
779 /* And then remount the systemd cgroup root read-only */
780 if (mount(NULL
, systemd_root
, NULL
, MS_BIND
|MS_REMOUNT
|MS_NOSUID
|MS_NOEXEC
|MS_NODEV
|MS_RDONLY
, NULL
) < 0)
781 return log_error_errno(errno
, "Failed to mount cgroup root read-only: %m");
786 int setup_volatile_state(
787 const char *directory
,
789 bool userns
, uid_t uid_shift
, uid_t uid_range
,
790 const char *selinux_apifs_context
) {
792 _cleanup_free_
char *buf
= NULL
;
793 const char *p
, *options
;
798 if (mode
!= VOLATILE_STATE
)
801 /* --volatile=state means we simply overmount /var
802 with a tmpfs, and the rest read-only. */
804 r
= bind_remount_recursive(directory
, true);
806 return log_error_errno(r
, "Failed to remount %s read-only: %m", directory
);
808 p
= prefix_roota(directory
, "/var");
810 if (r
< 0 && errno
!= EEXIST
)
811 return log_error_errno(errno
, "Failed to create %s: %m", directory
);
813 options
= "mode=755";
814 r
= tmpfs_patch_options(options
, userns
, uid_shift
, uid_range
, selinux_apifs_context
, &buf
);
820 if (mount("tmpfs", p
, "tmpfs", MS_STRICTATIME
, options
) < 0)
821 return log_error_errno(errno
, "Failed to mount tmpfs to /var: %m");
827 const char *directory
,
829 bool userns
, uid_t uid_shift
, uid_t uid_range
,
830 const char *selinux_apifs_context
) {
832 bool tmpfs_mounted
= false, bind_mounted
= false;
833 char template[] = "/tmp/nspawn-volatile-XXXXXX";
834 _cleanup_free_
char *buf
= NULL
;
835 const char *f
, *t
, *options
;
840 if (mode
!= VOLATILE_YES
)
843 /* --volatile=yes means we mount a tmpfs to the root dir, and
844 the original /usr to use inside it, and that read-only. */
846 if (!mkdtemp(template))
847 return log_error_errno(errno
, "Failed to create temporary directory: %m");
849 options
= "mode=755";
850 r
= tmpfs_patch_options(options
, userns
, uid_shift
, uid_range
, selinux_apifs_context
, &buf
);
856 if (mount("tmpfs", template, "tmpfs", MS_STRICTATIME
, options
) < 0) {
857 r
= log_error_errno(errno
, "Failed to mount tmpfs for root directory: %m");
861 tmpfs_mounted
= true;
863 f
= prefix_roota(directory
, "/usr");
864 t
= prefix_roota(template, "/usr");
867 if (r
< 0 && errno
!= EEXIST
) {
868 r
= log_error_errno(errno
, "Failed to create %s: %m", t
);
872 if (mount(f
, t
, NULL
, MS_BIND
|MS_REC
, NULL
) < 0) {
873 r
= log_error_errno(errno
, "Failed to create /usr bind mount: %m");
879 r
= bind_remount_recursive(t
, true);
881 log_error_errno(r
, "Failed to remount %s read-only: %m", t
);
885 if (mount(template, directory
, NULL
, MS_MOVE
, NULL
) < 0) {
886 r
= log_error_errno(errno
, "Failed to move root mount: %m");
890 (void) rmdir(template);
899 (void) umount(template);
900 (void) rmdir(template);
904 VolatileMode
volatile_mode_from_string(const char *s
) {
908 return _VOLATILE_MODE_INVALID
;
910 b
= parse_boolean(s
);
916 if (streq(s
, "state"))
917 return VOLATILE_STATE
;
919 return _VOLATILE_MODE_INVALID
;