diff --git a/Makefile b/Makefile index 72d151ce..fba9cf49 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,8 @@ SRCS = $(SRC_DIR)/main.c \ $(SRC_DIR)/x11.c \ $(SRC_DIR)/virgl-android.c \ $(SRC_DIR)/pulseaudio-android.c \ - $(SRC_DIR)/virtualize.c + $(SRC_DIR)/virtualize.c \ + $(SRC_DIR)/create_img.c # Compiler flags - hardened warning set, all warnings are errors CFLAGS = -Wall -Wextra -Wpedantic -Werror -O2 -flto=auto -std=gnu99 -I$(SRC_DIR) -no-pie -pthread diff --git a/src/create_img.c b/src/create_img.c new file mode 100644 index 00000000..03794265 --- /dev/null +++ b/src/create_img.c @@ -0,0 +1,387 @@ +/* + * Droidspaces v6 - High-performance Container Runtime + * + * Copyright (C) 2026 ravindu644 + * SPDX-License-Identifier: GPL-3.0-or-later + */ +/* + * DroidSpaces Image Creation + * + * Implements the "create" command for generating an ext4 rootfs image + * from a rootfs archive. + * + * Workflow: + * Create image -> Format ext4 -> Mount -> Extract archive + * -> Sync -> Unmount -> Cleanup + */ +#include "droidspace.h" + + +/* Busybox bundled with DroidSpaces (Android only) */ +#define DS_BUSYBOX DS_WORKSPACE_ANDROID "/bin/busybox" + +static int parse_size(const char *size, off_t *bytes); +static int create_sparse_image(const char *image, off_t bytes); +static int run_cmd(const char *const argv[]); +static int attach_loop(const char *image, char *loop_out, size_t loop_sz); +static void detach_loop(const char *loop_dev); +static int make_mount_point(char *path_out, size_t size); +static int is_valid_archive(const char *path); +static int extract_archive(const char *archive, const char *dest); + +int ds_create_image(const char *archive, const char *image, const char *size) +{ + off_t bytes; + char loop_dev[64] = {0}; + char mount_pt[256] = {0}; + int mounted = 0; + int ret = -1; + + /* ------------------------------------------------------------------ */ + /* Validations */ + /* ------------------------------------------------------------------ */ + if (getuid() != 0) { + ds_error("ds_create_image requires root"); + return -1; + } + if (!archive || !*archive) { + ds_error("Missing rootfs archive"); + return -1; + } + if (!image || !*image) { + ds_error("Missing image path"); + return -1; + } + if (!size || !*size) { + ds_error("Missing image size"); + return -1; + } + if (access(archive, F_OK) != 0) { + ds_error("Archive not found: %s", archive); + return -1; + } + if (!is_valid_archive(archive)) { + ds_error("Not a valid tar archive: %s", archive); + return -1; + } + if (parse_size(size, &bytes) < 0) { + ds_error("Invalid size: %s", size); + return -1; + } + if (access(image, F_OK) == 0) { + ds_error("Image already exists: %s", image); + return -1; + } + + /* ------------------------------------------------------------------ */ + /* Step 1: Create sparse image file */ + /* ------------------------------------------------------------------ */ + ds_log("Creating image: %s (%s)", image, size); + if (create_sparse_image(image, bytes) < 0) + return -1; + + /* ------------------------------------------------------------------ */ + /* Step 2: Format ext4 */ + /* ------------------------------------------------------------------ */ + ds_log("Formatting ext4 filesystem"); + const char *mkfs[] = { + "mkfs.ext4", + "-L " DS_PROJECT_NAME, + "-F", + image, + NULL + }; + if (run_cmd(mkfs) != 0) { + ds_error("mkfs.ext4 failed"); + goto err_unlink; + } + + /* ------------------------------------------------------------------ */ + /* Step 3: Attach loop device */ + /* ------------------------------------------------------------------ */ + ds_log("Attaching loop device"); + if (attach_loop(image, loop_dev, sizeof(loop_dev)) < 0) + goto err_unlink; + + /* ------------------------------------------------------------------ */ + /* Step 4: Create mount point and mount */ + /* ------------------------------------------------------------------ */ + if (make_mount_point(mount_pt, sizeof(mount_pt)) < 0) + goto err_detach; + + ds_log("Mounting %s -> %s", loop_dev, mount_pt); + if (mount(loop_dev, mount_pt, "ext4", 0, NULL) != 0) { + ds_error("mount(%s, %s): %s", loop_dev, mount_pt, strerror(errno)); + rmdir(mount_pt); + goto err_detach; + } + mounted = 1; + + /* ------------------------------------------------------------------ */ + /* Step 5: Extract archive */ + /* ------------------------------------------------------------------ */ + ds_log("Extracting archive: %s", archive); + if (extract_archive(archive, mount_pt) != 0) { + ds_error("Archive extraction failed"); + goto err_unmount; + } + + /* ------------------------------------------------------------------ */ + /* Step 6: Sync */ + /* ------------------------------------------------------------------ */ + ds_log("Syncing filesystem"); + sync(); + + ds_log("Image created successfully: %s", image); + ret = 0; + + /* ------------------------------------------------------------------ */ + /* Cleanup */ + /* ------------------------------------------------------------------ */ +err_unmount: + if (mounted) { + if (umount2(mount_pt, 0) != 0) + ds_warn("umount2(%s): %s", mount_pt, strerror(errno)); + rmdir(mount_pt); + } +err_detach: + detach_loop(loop_dev); +err_unlink: + if (ret != 0) + unlink(image); + return ret; +} + +/* -------------------------------------------------------------------------- + * Archive validation: check magic bytes, not filename extension. + * + * Recognised magic: + * xz: FD 37 7A 58 5A 00 + * gzip: 1F 8B + * bzip2: 42 5A 68 + * tar: "ustar" at offset 257 (POSIX) or offset 265 (GNU) + * -------------------------------------------------------------------------- */ + +static int is_valid_archive(const char *path) { + unsigned char buf[512] = {0}; + int fd; + ssize_t n; + + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return 0; + n = read(fd, buf, sizeof(buf)); + close(fd); + if (n < 6) + return 0; + + /* xz */ + if (buf[0] == 0xFD && buf[1] == '7' && buf[2] == 'z' && + buf[3] == 'X' && buf[4] == 'Z' && buf[5] == 0x00) + return 1; + + /* gzip */ + if (buf[0] == 0x1F && buf[1] == 0x8B) + return 1; + + /* bzip2 */ + if (buf[0] == 'B' && buf[1] == 'Z' && buf[2] == 'h') + return 1; + + /* bare tar: "ustar" at offset 257 */ + if (n >= 262 && memcmp(buf + 257, "ustar", 5) == 0) + return 1; + + return 0; +} + +/* -------------------------------------------------------------------------- + * Archive extraction + * + * Linux: system tar handles xz natively via -J flag. + * Android: toybox tar has no xz support and no system xz binary. + * Use bundled busybox + * -------------------------------------------------------------------------- */ +static int extract_archive(const char *archive, const char *dest) +{ + const char *args[8]; + int i = 0; + + if (is_android()) { + args[i++] = DS_BUSYBOX; + args[i++] = "tar"; + } else { + args[i++] = "tar"; + } + + args[i++] = "-xpJf"; + args[i++] = archive; + args[i++] = "-C"; + args[i++] = dest; + args[i++] = "--numeric-owner"; + args[i++] = NULL; + + return run_cmd(args); +} + +/* -------------------------------------------------------------------------- + * Loop device helpers + * -------------------------------------------------------------------------- */ + +static int attach_loop(const char *image, char *loop_out, size_t loop_sz) +{ + int ctrl_fd = -1; + int img_fd = -1; + int loop_fd = -1; + int free_num = -1; + + ctrl_fd = open("/dev/loop-control", O_RDWR | O_CLOEXEC); + if (ctrl_fd < 0) { + ds_error("open(/dev/loop-control): %s", strerror(errno)); + return -1; + } + + free_num = ioctl(ctrl_fd, LOOP_CTL_GET_FREE); + close(ctrl_fd); + if (free_num < 0) { + ds_error("LOOP_CTL_GET_FREE: %s", strerror(errno)); + return -1; + } + + snprintf(loop_out, loop_sz, + is_android() ? "/dev/block/loop%d" : "/dev/loop%d", + free_num); + + img_fd = open(image, O_RDWR | O_CLOEXEC); + if (img_fd < 0) { + ds_error("open(%s): %s", image, strerror(errno)); + return -1; + } + + loop_fd = open(loop_out, O_RDWR | O_CLOEXEC); + if (loop_fd < 0) { + ds_error("open(%s): %s", loop_out, strerror(errno)); + close(img_fd); + return -1; + } + + if (ioctl(loop_fd, LOOP_SET_FD, img_fd) < 0) { + ds_error("LOOP_SET_FD(%s): %s", loop_out, strerror(errno)); + close(loop_fd); + close(img_fd); + return -1; + } + + /* Optional but good practice: set loop info */ + struct loop_info64 info; + memset(&info, 0, sizeof(info)); + strncpy((char *)info.lo_file_name, + image, + sizeof(info.lo_file_name) - 1); + ioctl(loop_fd, LOOP_SET_STATUS64, &info); /* non-fatal if it fails */ + + close(loop_fd); + close(img_fd); + return 0; +} + +static void detach_loop(const char *loop_dev) +{ + if (!loop_dev || !*loop_dev) + return; + int fd = open(loop_dev, O_RDWR | O_CLOEXEC); + if (fd < 0) + return; + if (ioctl(fd, LOOP_CLR_FD, 0) < 0) + ds_warn("LOOP_CLR_FD(%s): %s", loop_dev, strerror(errno)); + close(fd); +} + +/* -------------------------------------------------------------------------- + * Mount point: Based on OS + * -------------------------------------------------------------------------- */ + +static int make_mount_point(char *path_out, size_t size) +{ + const char *base = is_android() ? "/data/local/tmp/container-XXXXXX" + : "/tmp/container-XXXXXX"; + snprintf(path_out, size, "%s", base); + + if (!mkdtemp(path_out)) { + ds_error("mkdtemp(%s): %s", base, strerror(errno)); + return -1; + } + return 0; +} + +/* -------------------------------------------------------------------------- + * run_cmd + * -------------------------------------------------------------------------- */ + +static int run_cmd(const char *const argv[]) +{ + pid_t pid; + int status; + + pid = fork(); + if (pid < 0) { + ds_error("fork failed: %s", strerror(errno)); + return -1; + } + if (pid == 0) { + execvp(argv[0], (char *const *)(const void *)argv); + perror(argv[0]); + _exit(127); + } + if (waitpid(pid, &status, 0) < 0) { + ds_error("waitpid failed: %s", strerror(errno)); + return -1; + } + if (!WIFEXITED(status)) + return -1; + return WEXITSTATUS(status); +} + +/* -------------------------------------------------------------------------- + * Sparse image creation + * -------------------------------------------------------------------------- */ + +static int create_sparse_image(const char *image, off_t bytes) +{ + int fd = open(image, O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { + ds_error("open(%s): %s", image, strerror(errno)); + return -1; + } + if (ftruncate(fd, bytes) < 0) { + ds_error("ftruncate(%s): %s", image, strerror(errno)); + close(fd); + return -1; + } + close(fd); + return 0; +} + +/* -------------------------------------------------------------------------- + * Size parser: accepts G/M/K suffixes (case-insensitive) or raw bytes + * -------------------------------------------------------------------------- */ + +static int parse_size(const char *str, off_t *bytes) +{ + char *end; + unsigned long long value = strtoull(str, &end, 10); + + if (end == str) + return -1; + + switch (*end) { + case 'G': case 'g': value *= 1024ULL * 1024ULL * 1024ULL; break; + case 'M': case 'm': value *= 1024ULL * 1024ULL; break; + case 'K': case 'k': value *= 1024ULL; break; + case '\0': break; + default: return -1; + } + + *bytes = (off_t)value; + return 0; +} diff --git a/src/droidspace.h b/src/droidspace.h index ed6a56cf..fc8489b4 100644 --- a/src/droidspace.h +++ b/src/droidspace.h @@ -49,6 +49,7 @@ #include #include #include +#include #ifndef RAMFS_MAGIC #define RAMFS_MAGIC 0x858458f6 @@ -846,4 +847,9 @@ int ds_daemon_run(int foreground, char **argv); int ds_client_run(int argc, char **argv); int ds_daemon_probe(void); +/* --------------------------------------------------------------------------- + * create_img.c + * ---------------------------------------------------------------------------*/ +int ds_create_image(const char *archive, const char *image, const char *size); + #endif /* DROIDSPACE_H */ diff --git a/src/main.c b/src/main.c index 18753dd1..274c80bd 100644 --- a/src/main.c +++ b/src/main.c @@ -26,6 +26,7 @@ void print_usage(void) { printf( "Usage: droidspaces [options] [args]\n\n" C_BOLD "Commands:" C_RESET "\n" + " create Create container image file\n" " start Start a new container\n" " stop Stop one or more containers\n" " restart Restart a container\n" @@ -42,7 +43,12 @@ void print_usage(void) { " version Show version information\n" " daemon Run daemon mode (use --foreground for " "foreground execution)\n\n" - + + C_BOLD "Options (Image Creation):" C_RESET "\n" + " -A, --rootfs-arc=PATH Path to os rootfs archive file\n" + " -i, --rootfs-img=PATH Path to rootfs image (.img)\n" + " -s, --size=NAME Size of image.(in GB only, eg. 2G, 10G, 4G)\n\n" + C_BOLD "Options (Container Setup):" C_RESET "\n" " -r, --rootfs=PATH Path to rootfs directory\n" " -i, --rootfs-img=PATH Path to rootfs image (.img)\n" @@ -77,8 +83,9 @@ void print_usage(void) { " --virgl-flags=\"FLAGS\" Extra flags passed to " "virgl_test_server_android\n" " --pulse-audio Configure PulseAudio sound server " - "support\n\n" - + "support\n\n"); + + printf( C_BOLD "Options (Security & Boot):" C_RESET "\n" " -P, --selinux-permissive Set host SELinux to permissive mode\n" " -V, --volatile Discard changes on exit (OverlayFS)\n" @@ -338,6 +345,8 @@ int main(int argc, char **argv) { safe_strncpy(cfg.prog_name, argv[0], sizeof(cfg.prog_name)); static struct option long_options[] = { + {"size", required_argument, 0, 's'}, + {"rootfs-arc", required_argument, 0, 'A'}, {"rootfs", required_argument, 0, 'r'}, {"rootfs-img", required_argument, 0, 'i'}, {"name", required_argument, 0, 'n'}, @@ -395,7 +404,7 @@ int main(int argc, char **argv) { * 3. Override Pass: Apply CLI overrides on top of loaded config. */ const char *discovered_cmd = NULL; - char temp_r[PATH_MAX] = {0}, temp_i[PATH_MAX] = {0}; + char temp_r[PATH_MAX] = {0}, temp_i[PATH_MAX] = {0}, temp_s[PATH_MAX] = {0}, temp_f[PATH_MAX] = {0}; char run_user[256] = {0}; int reset_config = 0; int cli_net_mode_set = 0; @@ -404,8 +413,7 @@ int main(int argc, char **argv) { /* 1. Discovery Pass: Capture identity and command without permuting argv. * Using '-' at the start of optstring returns non-options as '1'. */ - while ((opt = getopt_long(argc, argv, "-r:i:n:h:d:fHXPvVB:C:E:u:", - long_options, NULL)) != -1) { + while ((opt = getopt_long(argc, argv, "-r:i:A:s:n:h:d:fHXPvVB:C:E:u:", long_options, NULL)) != -1) { if (opt == 1) { /* Non-option argument */ if (!discovered_cmd) { discovered_cmd = optarg; @@ -427,6 +435,10 @@ int main(int argc, char **argv) { safe_strncpy(temp_r, optarg, sizeof(temp_r)); } else if (opt == 'i') { safe_strncpy(temp_i, optarg, sizeof(temp_i)); + } else if (opt == 's') { + safe_strncpy(temp_s, optarg, sizeof(temp_s)); + } else if (opt == 'A') { + safe_strncpy(temp_f, optarg, sizeof(temp_f)); } else if (opt == 'u') { safe_strncpy(run_user, optarg, sizeof(run_user)); } else if (opt == 256) { @@ -448,8 +460,14 @@ int main(int argc, char **argv) { } } } + optind = 0; /* Reset for next steps */ + if (discovered_cmd && strcmp(discovered_cmd, "create") == 0) + { + return ds_create_image(temp_f, temp_i, temp_s); + } + /* * Daemon Proxying: * Optimistically attempt to proxy commands to the background daemon. @@ -461,8 +479,7 @@ int main(int argc, char **argv) { * Commands that do not require root access (docs, help, version) or * must be run locally to avoid recursive loops (mode) are never proxied. */ - int is_stateless_cmd = - (discovered_cmd && (strcmp(discovered_cmd, "docs") == 0 || + int is_stateless_cmd = (discovered_cmd && (strcmp(discovered_cmd, "docs") == 0 || strcmp(discovered_cmd, "help") == 0 || strcmp(discovered_cmd, "version") == 0 || strcmp(discovered_cmd, "mode") == 0)); @@ -484,8 +501,7 @@ int main(int argc, char **argv) { * /Containers//container.config if config hasn't * been loaded yet. */ - int is_stateful = - (discovered_cmd && (strcmp(discovered_cmd, "stop") == 0 || + int is_stateful = (discovered_cmd && (strcmp(discovered_cmd, "stop") == 0 || strcmp(discovered_cmd, "restart") == 0 || strcmp(discovered_cmd, "pid") == 0 || strcmp(discovered_cmd, "info") == 0 || @@ -558,7 +574,7 @@ int main(int argc, char **argv) { * Strict mode for 'run' prevents stealing arguments from the sub-command. */ int strict = (discovered_cmd && (strcmp(discovered_cmd, "run") == 0)); const char *optstring = - strict ? "+r:i:n:h:d:fHXPvVB:C:E:u:" : "r:i:n:h:d:fHXPvVB:C:E:u:"; + strict ? "+r:i:s:A:n:h:d:fHXPvVB:C:E:u:" : "r:i:s:A:n:h:d:fHXPvVB:C:E:u:"; while ((opt = getopt_long(argc, argv, optstring, long_options, NULL)) != -1) { switch (opt) { @@ -918,6 +934,7 @@ int main(int argc, char **argv) { } break; } + case 260: /* --force-cgroupv1: escape hatch to legacy hierarchy */ cfg.force_cgroupv1 = 1;