From: Stefan Schantl Date: Fri, 21 Mar 2025 20:01:30 +0000 (+0100) Subject: btrfsctrl: New suid binary helper program X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=9f15a18f5d666d3078ea1d312f8ba7076b8b099c;p=people%2Fstevee%2Fipfire-2.x.git btrfsctrl: New suid binary helper program This binary is used to show, create, delete and restory snapshots on a BTRFS installed system. Signed-off-by: Stefan Schantl --- diff --git a/config/rootfiles/common/misc-progs b/config/rootfiles/common/misc-progs index d6594b3f8..33cf82f2b 100644 --- a/config/rootfiles/common/misc-progs +++ b/config/rootfiles/common/misc-progs @@ -1,5 +1,6 @@ usr/local/bin/addonctrl usr/local/bin/backupctrl +usr/local/bin/btrfscrtl usr/local/bin/captivectrl #usr/local/bin/clamavctrl usr/local/bin/collectdctrl diff --git a/src/misc-progs/Makefile b/src/misc-progs/Makefile index 1ae12b294..610eb5aec 100644 --- a/src/misc-progs/Makefile +++ b/src/misc-progs/Makefile @@ -32,7 +32,7 @@ SUID_PROGS = squidctrl sshctrl ipfirereboot \ smartctrl clamavctrl addonctrl pakfire wlanapctrl \ setaliases urlfilterctrl updxlratorctrl fireinfoctrl rebuildroutes \ getconntracktable wirelessclient torctrl ddnsctrl unboundctrl \ - captivectrl + captivectrl btrfsctrl OBJS = $(patsubst %,%.o,$(PROGS) $(SUID_PROGS)) @@ -55,3 +55,6 @@ setuid.o: setuid.c setuid.h $(PROGS) $(SUID_PROGS): setuid.o | $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $@.o $< $(LIBS) + +btrfsctrl: btrfsctrl.o + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ setuid.o $< $(LIBS) -lbtrfsutil diff --git a/src/misc-progs/btrfsctrl.c b/src/misc-progs/btrfsctrl.c new file mode 100644 index 000000000..016ecb972 --- /dev/null +++ b/src/misc-progs/btrfsctrl.c @@ -0,0 +1,642 @@ +/* This file is part of the IPFire Firewall. + * + * This program is distributed under the terms of the GNU General Public + * Licence. See the file COPYING for details. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "setuid.h" + +#include "btrfsutil.h" +#include "initreq.h" + +#define BTRFSPROG "/usr/bin/btrfs" +#define SNAPSHOTDIR "/.snapshots" +#define ROOTDIR "/" +#define RESTOREDIR "/tmp/restore" +#define PROCMOUNTS "/proc/mounts" + +#define BACKUP_PREFIX "auto-moving-to" + +#define VALID_NAME_CHARS "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_" +#define NUMBERS "0123456789" +#define BTRFS_FS_TREE_OBJECTID 5 + +/* + * _btrfsctrl_validate_name Function + * + * Validates if a given snapshot name only contains allowed characters. + * +*/ +static int _btrfsctrl_validate_name (const char *name) { + // Check if the given snapshot name only contains allowed characters. + if( strspn(name, VALID_NAME_CHARS) != strlen(name)) { + return -1; + } + + return 0; +} + +/* + * _btrfsctrl_validate_number Function + * + * Validates if a given number of type char only contains digits. + * +*/ +static int _btrfsctrl_validate_number(const char *number) { + // Check if the given id is a valid number. + if( strspn(number, NUMBERS) != strlen(number)) { + return -1; + } + + return 0; +} + +/* + * _btrfsctrl_convert_str_to_uint64_t Function + * + * Converts a number with type char into a unsigned long intager. + * + * In case a number has been passed as argv it's type is char, but a lot of + * functions needs them as long int. + * +*/ +static unsigned long int _btrfsctrl_convert_str_to_uint64_t(const char *number) { + // Convert the given number string to unsigned long int. + uint64_t long_uint = strtoul (number, NULL, 0); + + return long_uint; +} + + +/* + * btrfsctrl_create_snapshot Function + * + * Requires a given name and True/False to set if a the snapshot should be read-only. + * + */ +static int btrfsctrl_create_snapshot (const char *name, bool read_only) { + char destination[STRING_SIZE]; + int ro_flag = 0; + + // Set read-only flag if requested. + if(read_only == true) { + ro_flag = BTRFS_UTIL_CREATE_SNAPSHOT_READ_ONLY; + } + + // Build destination directory. + snprintf(destination, sizeof(destination), "%s/%s", SNAPSHOTDIR, name); + + // Create the snapshot in read-only mode. + int ret = btrfs_util_subvolume_snapshot(ROOTDIR, destination, ro_flag, NULL, NULL); + if (ret != BTRFS_UTIL_OK) { + // Get the error message. + const char *error = btrfs_util_strerror(ret); + fprintf(stderr, "Could not create snapshot: %s\n", error); + return -1; + } + + return 0; +} + +/* + * _btrfsctrl_create_directory Function + * + * Function to create the defined RESTOREDIR in case it does not exist yet. + * +*/ +static int _btrfsctrl_create_directory(const char *directory) { + // Try to access the directory. + if (access(RESTOREDIR, X_OK) != 0) { + // Create the directory. + int ret = mkdir(directory, S_IRWXU|S_IRWXG|S_IRWXO); + + // Handle error code. + if (ret != 0 && errno != EEXIST) { + return -1; + } + } + + return 0; +} + +/* + * _btrfsctrl_get_root_device Function. + * + * This function will return the device where root is currently mounted to. + * +*/ + +static const char * _btrfsctrl_get_root_device () { + struct mntent *ent; + FILE *mounts; + + // Open kernel proc filesystem. + mounts = setmntent(PROCMOUNTS, "r"); + if (mounts == NULL) { + return false; + } + + // Loop through all known mount points. + while (NULL != (ent = getmntent(mounts))) { + // Check if the current processed mount point is the root filesystem. + if (strcmp(ent->mnt_dir, ROOTDIR) == 0) { + break; + } + } + + // Close proc filesystem. + endmntent(mounts); + + return ent->mnt_fsname; +} + +/* + * _btrfsctrl_mount_btrfs_lvl5 Function. + * + * Function to mount the top level of a BTRFS. + * +*/ +static int _btrfsctrl_mount_btrfs_lvl5(const char *device) { + char mount_options[STRING_SIZE]; + + // Assign mount options. + snprintf(mount_options, sizeof(mount_options), "subvolid=%d", BTRFS_FS_TREE_OBJECTID); + + // Mount the top level BTRFS filesystem. + int ret = mount(device, RESTOREDIR, "btrfs", 0, mount_options); + if (ret < 0) { + return -1; + } + + return 0; +} + +/* + * _btrfsctrl_umount_btrfs_lvl5 Function + * + * Umounts a mounted top level BTRFS from the defined restore directory. + * +*/ + +static int _btrfsctrl_umount_btrfs_lvl5() { + int retry = 1; + int counter = 0; + + while(1) { + // Reset the retry marker + retry = 0; + + // Try to umount the filesystem. + int ret = umount2(RESTOREDIR, 0); + if (ret) { + switch(errno) { + case EBUSY: + // Set marker to retry the umount. + retry = 1; + + // Ignore if the rootfs could not be unmounted yet, + // because it is still used. + break; + case EINVAL: + // Ignore if the rootfs already has been unmounted + break; + case ENOENT: + // Ignore if the directory does not longer exist. + break; + default: + return ret; + } + } + + // Abort loop if the rootfs got umounted + if (retry == 0) { + return 0; + } + + // Abort after five failed umount attempts + if (counter == 5) { + return -1; + } + + // Increment counter. + counter++; + } +} + +/* + * _btrfsctrl_generate_backup_name Function + * + * Function which generates and returns a backup name for the old root + * when booting into or restoring a snapshot. + * +*/ + +static const char * _btrfsctrl_generate_backup_name (const char *path) { + time_t my_time; + struct tm * timeinfo; + char *backup_name = NULL; + char *pathc = NULL; + char *bname = NULL; + + // Use basename function to get the name of the given path. + pathc = strdup(path); + bname = basename(pathc); + + // Get the current time. + time (&my_time); + timeinfo = localtime (&my_time); + + // Generate name for backup of the current rootdir. + asprintf(&backup_name, "@snapshots/%d-%02d-%02d-%02d-%02d-%s-%s", timeinfo->tm_year+1900, timeinfo->tm_mon+1, timeinfo->tm_mday, + timeinfo->tm_hour, timeinfo->tm_min, BACKUP_PREFIX, bname); + + return backup_name; +} + +/* + * _btrfsctrl_generate_abs_backup_path Function + * + * Function which will require a path as input and will return the absolute path from defined backupdir. + * +*/ +static const char * _btrfsctrl_generate_abs_path (const char *prefix, const char *path) { + char *abs_path = NULL; + + //Generate absolute path. + asprintf(&abs_path, "%s/\%s", prefix, path); + + return abs_path; +} + +/* + * _btrfsctrl_call_for_reboot Function + * + * This functions uses the sysvinit FIFO to talk to initd and request for a reboot. + * +*/ +static int _btrfsctrl_call_for_reboot() { + struct init_request request; + int fd; + + // Allocate some memory for the request. + memset(&request, 0, sizeof(request)); + + // Define the request to change to runlevel six (reboot) + request.magic = INIT_MAGIC; + request.cmd = INIT_CMD_RUNLVL; + request.runlevel = '6'; + + // Open the FIFO pipe to the init process and send the request. + if ((fd = open(INIT_FIFO, O_WRONLY)) >= 0) { + ssize_t p = 0; + size_t s = sizeof(request); + void *ptr = &request; + + while (s > 0) { + p = write(fd, ptr, s); + if (p < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + break; + } + ptr += p; + s -= p; + } + close(fd); + } + + return 0; + +} + +int main(int argc, char *argv[]) { + char command[STRING_SIZE]; + + if (!(initsetuid())) + exit(1); + + if (argc < 2) { + fprintf(stderr, "\nNo argument given.\n\nbtrfsctrl (filesystem-usage|subvolume-list|snapshot-create|snapshot-delete|snapshot-restore)\n\n"); + exit(1); + } + + if (strcmp(argv[1], "filesystem-usage") == 0) { + snprintf(command, sizeof(command), BTRFSPROG " filesystem usage -b %s", ROOTDIR); + safe_system(command); + + // Get a list of all known subvolumes. + } else if (strcmp(argv[1], "subvolume-list") == 0) { + struct btrfs_util_subvolume_iterator* subvolume_iterator = NULL; + struct btrfs_util_subvolume_info subvolume_info; + char *path = NULL; + int ret; + + // Create new subvolume iterator. + ret = btrfs_util_subvolume_iter_create(ROOTDIR, BTRFS_FS_TREE_OBJECTID, 0, &subvolume_iterator); + if (ret != BTRFS_UTIL_OK) { + // Get the error message. + const char *error = btrfs_util_strerror(ret); + fprintf(stderr, "Could not create subvolumr iterator.\n%s\n", error); + exit(1); + } + + // Init and iterate over the first subvolume. + ret = btrfs_util_subvolume_iter_next_info(subvolume_iterator, &path, &subvolume_info); + if(ret != BTRFS_UTIL_OK) { + // Get the error message. + const char *error = btrfs_util_strerror(ret); + fprintf(stderr, "Could not iterate over the first subvolume.\n%s\n", error); + + // Destroy the subvolume iterator. + btrfs_util_subvolume_iter_destroy(subvolume_iterator); + + // Free the path, if set. + if (path != NULL) { + free(path); + } + + exit(1); + } + + // Iterate over the existing subvolumes/snapshots. + while(path) { + // Generate the line to print. + printf("%ld %s %ld\n", subvolume_info.id, path, subvolume_info.parent_id); + + // Free the path, if set. + if (path != NULL) { + free(path); + } + + // Iterate over the next subvolume. + ret = btrfs_util_subvolume_iter_next_info(subvolume_iterator, &path, &subvolume_info); + + // Handle return value. + if (ret == BTRFS_UTIL_ERROR_STOP_ITERATION) { + // Nothing more to iterate. + break; + + } else if ( ret != BTRFS_UTIL_OK) { + // Get the error message. + const char *error = btrfs_util_strerror(ret); + + fprintf(stderr, "Could not iterate over the next subvolume. \n%s\n", error); + + // Destroy the subvolume iterator. + btrfs_util_subvolume_iter_destroy(subvolume_iterator); + + // Free the path if set. + if (path != NULL) { + free(path); + } + exit(1); + } + } + + // Destroy the subvolume iterator. + btrfs_util_subvolume_iter_destroy(subvolume_iterator); + + // Create a snapshot. + } else if (strcmp(argv[1], "snapshot-create") == 0) { + int r; + + // Check if the given snapshot name is valid. + r = _btrfsctrl_validate_name(argv[2]); + if (r < 0) { + fprintf(stderr, "\nInvalid snapshot name. \nValid characters are: %s\n\n", VALID_NAME_CHARS); + exit(1); + } + + // Call function and create a read-only snapshot with the given name. + r = btrfsctrl_create_snapshot(argv[2], true); + if (r < 0) { + exit(1); + } + + // Delete a snapshot by it's given ID + } else if (strcmp(argv[1], "snapshot-delete") == 0) { + int ret; + + // Check if the given id is a valid number. + ret = _btrfsctrl_validate_number(argv[2]); + if (ret < 0) { + fprintf(stderr, "\n Invalid snaphot ID: Not a numerical input.\n\n"); + exit(1); + } + + // Convert the given id string to unsigned long int. + uint64_t id = _btrfsctrl_convert_str_to_uint64_t(argv[2]); + + // Open a file descriptor to the SNAPSHOTDIR. + int fd = open(SNAPSHOTDIR, O_DIRECTORY); + if (fd < 0) { + fprintf(stderr, "\nCould not open %s - Code: %d\n", SNAPSHOTDIR, fd); + exit(1); + } + + // Delte the snapshot with the given ID. + ret = btrfs_util_delete_subvolume_by_id_fd(fd, id); + if (ret != BTRFS_UTIL_OK) { + // Get error message. + const char *error = btrfs_util_strerror(ret); + fprintf(stderr, "%s\n", error); + + // Close file descriptor. + close(fd); + exit(1); + } + + // Close file desriptor. + close(fd); + + // Restore a snapshot with a given ID. + } else if (strcmp(argv[1], "snapshot-restore") == 0) { + int ret; + char *path = NULL; + bool mounted = false; + + // Check if the given id is a valid number. + ret = _btrfsctrl_validate_number(argv[2]); + if(ret < 0) { + fprintf(stderr, "\n Invalid snapshot ID. Not a numerical input.\n\n"); + + goto ERROR; + } + + // Convert the given id string to unsigned long int. + uint64_t id = _btrfsctrl_convert_str_to_uint64_t(argv[2]); + + // Get the path of the given snapshot ID. + ret = btrfs_util_subvolume_get_path(ROOTDIR, id, &path); + if (ret != BTRFS_UTIL_OK) { + // Get error message. + const char *error = btrfs_util_strerror(ret); + fprintf(stderr, "%s\n", error); + + goto ERROR; + } + + // Create restore directory. + ret = _btrfsctrl_create_directory(RESTOREDIR); + if (ret < 0) { + // Get the error message. + const char *error = strerror(errno); + fprintf(stderr, "Could not create %s %s\n", RESTOREDIR, error); + + goto ERROR; + } + + // Get the device name of the mounted root partition. + const char *rootdev = _btrfsctrl_get_root_device(); + if (!rootdev) { + fprintf(stderr, "Could not get root device.\n"); + + goto ERROR; + } + + // Mount the top level of the BTRFS. + ret = _btrfsctrl_mount_btrfs_lvl5(rootdev); + if (ret < 0) { + // Get error message. + const char *error = strerror(errno); + fprintf(stderr, "Could not mount top level BTRFS to %s. %s\n", RESTOREDIR, error); + + goto ERROR; + } + + // Set mounted to true. + mounted = true; + + // Generate a backup name. + const char *backup_name = _btrfsctrl_generate_backup_name(path); + if (!backup_name) { + fprintf(stderr, "Could not generate a backup name.\n"); + + goto ERROR; + } + + // Generate absolute path values. + const char *oldname = _btrfsctrl_generate_abs_path(RESTOREDIR, "@"); + const char *newname = _btrfsctrl_generate_abs_path(RESTOREDIR, backup_name); + if ((!oldname) || (!newname)) { + fprintf(stderr, "Could not generate absolute path values.\n"); + + goto ERROR; + } + + // Rename / Move the current @ to backup location + ret = rename(oldname, newname); + if (ret < 0) { + if (errno == ENOENT) { + fprintf(stderr, "ENOENT\n"); + } + + // Get error message. + const char *error = strerror(errno); + fprintf(stderr, "Could not backup/move current root filesystem. %s\n", error); + + goto ERROR; + } + + /* + * XXX + * + * Needs some more attention. When swithing the old/current root to read-only no write + * operations are possible. So the RESTOREDIT could not be deleted correctly afterwards + * and also during init reboot the services can not write anything back to disk. + */ + // Set the backup of root dir to read-only. + /* + ret = btrfs_util_subvolume_set_read_only(newname, true); + if (ret != BTRFS_UTIL_OK) { + // Get error message. + const char *error = btrfs_util_strerror(ret); + fprintf(stderr, "Could not change the rootfs backup to read-only. %s\n", error); + + goto ERROR; + } + */ + + // Create a snapshot of the given snapshot ID as the new root. + const char *restore = _btrfsctrl_generate_abs_path(RESTOREDIR, path); + ret = btrfs_util_subvolume_snapshot(restore, oldname, 0, NULL, NULL); + if (ret > 0) { + // Get the error message. + const char *error = btrfs_util_strerror(ret); + fprintf(stderr, "Could not restore snapshot %s: %s\n", path, error); + + goto ERROR; + } + + + + // Call sync to sync the disks + sync(); + + // Umount the BTRFS root filesystem from restore directory. + ret = _btrfsctrl_umount_btrfs_lvl5(); + if (ret < 0) { + // Get error message + const char *error = strerror(errno); + fprintf(stderr, "Could not umount BTRFS root filesystem from %s. %s\n", RESTOREDIR, error); + + goto ERROR; + } + + // Set mounted to false again. + mounted = false; + + // Remove the restore directory. + ret = rmdir(RESTOREDIR); + if(ret != 0) { + fprintf(stderr, "Could not remove %s. %d\n", RESTOREDIR, ret); + + goto ERROR; + } + + // Reboot the system + ret = _btrfsctrl_call_for_reboot(); + if (ret < 0) { + fprintf(stderr, "Could not reboot the system.\n"); + exit(1); + } + + exit(0); + + ERROR: + // Umount the top root BTRFS in case it is mounted. + if (mounted == true) { + ret = _btrfsctrl_umount_btrfs_lvl5(); + if (ret < 0) { + // Get error message + const char *error = strerror(errno); + fprintf(stderr, "Could not umount BTRFS root filesystem during cleanup. %s\n", error); + } + } + + // Free path if set. + if(path != NULL) { + free(path); + } + + exit(1); + } else { + fprintf(stderr, "\nBad argument given.\n\nbtrfsctrl (filesystem-usage|subvolume-list|snapshot-create|snapshot-delete|snapshot-restore)\n\n"); + exit(1); + } + + return 0; +}