Skip to content

Commit c94df8e

Browse files
Copilotprobonopd
andcommitted
Add unshare functionality - initial implementation
Co-authored-by: probonopd <[email protected]>
1 parent 84c722f commit c94df8e

File tree

1 file changed

+270
-10
lines changed

1 file changed

+270
-10
lines changed

src/runtime/runtime.c

Lines changed: 270 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,28 @@ extern int sqfs_opt_proc(void* data, const char* arg, int key, struct fuse_args*
6464
#include <libgen.h>
6565
#include <dirent.h>
6666
#include <ctype.h>
67+
#include <sched.h>
68+
#include <sys/mount.h>
69+
#include <sys/prctl.h>
70+
#include <sys/syscall.h>
71+
#include <linux/capability.h>
6772

6873
const char* fusermountPath = NULL;
6974

75+
// Capability structures for namespace support
76+
#define _LINUX_CAPABILITY_VERSION_3 0x20080522
77+
78+
struct cap_header {
79+
uint32_t version;
80+
int pid;
81+
};
82+
83+
struct cap_data {
84+
uint32_t effective;
85+
uint32_t permitted;
86+
uint32_t inheritable;
87+
};
88+
7089
typedef struct {
7190
uint32_t lo;
7291
uint32_t hi;
@@ -414,6 +433,162 @@ int appimage_print_binary(char* fname, unsigned long offset, unsigned long lengt
414433
return 0;
415434
}
416435

436+
// Restore capabilities after entering user namespace
437+
void restore_capabilities(void) {
438+
struct cap_header caps = {
439+
.version = _LINUX_CAPABILITY_VERSION_3,
440+
.pid = 0
441+
};
442+
struct cap_data cap_data[2] = {{0, 0, 0}, {0, 0, 0}};
443+
444+
if (syscall(SYS_capget, &caps, &cap_data) == 0) {
445+
FILE* f = fopen("/proc/sys/kernel/cap_last_cap", "r");
446+
uint32_t last_cap = 39; // default fallback
447+
if (f != NULL) {
448+
if (fscanf(f, "%u", &last_cap) != 1) {
449+
last_cap = 39;
450+
}
451+
fclose(f);
452+
}
453+
454+
uint64_t all_caps = (1ULL << (last_cap + 1)) - 1;
455+
cap_data[0].effective = (uint32_t)(all_caps & 0xFFFFFFFF);
456+
cap_data[0].permitted = (uint32_t)(all_caps & 0xFFFFFFFF);
457+
cap_data[0].inheritable = (uint32_t)(all_caps & 0xFFFFFFFF);
458+
cap_data[1].effective = (uint32_t)((all_caps >> 32) & 0xFFFFFFFF);
459+
cap_data[1].permitted = (uint32_t)((all_caps >> 32) & 0xFFFFFFFF);
460+
cap_data[1].inheritable = (uint32_t)((all_caps >> 32) & 0xFFFFFFFF);
461+
462+
syscall(SYS_capset, &caps, &cap_data);
463+
464+
for (uint32_t cap = 0; cap <= last_cap; cap++) {
465+
prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap, 0, 0);
466+
}
467+
} else {
468+
fprintf(stderr, "Warning: failed to get capabilities: %s\n", strerror(errno));
469+
}
470+
}
471+
472+
// Make all mounts private in the mount namespace
473+
bool try_make_mount_private(void) {
474+
if (mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL) == 0) {
475+
return true;
476+
}
477+
return false;
478+
}
479+
480+
// Check if we're already in a user and mount namespace
481+
bool is_in_user_and_mount_namespace(void) {
482+
FILE* f = fopen("/proc/self/uid_map", "r");
483+
if (f == NULL) {
484+
return false;
485+
}
486+
487+
char line[256];
488+
bool result = false;
489+
490+
if (fgets(line, sizeof(line), f) != NULL) {
491+
// Parse the uid_map: "target_uid host_uid count"
492+
uint32_t target_uid, host_uid, count;
493+
if (sscanf(line, "%u %u %u", &target_uid, &host_uid, &count) == 3) {
494+
// If count is less than full range (4294967295), we're in a user namespace
495+
if (count < 4294967295) {
496+
result = try_make_mount_private();
497+
}
498+
}
499+
}
500+
501+
fclose(f);
502+
return result;
503+
}
504+
505+
// Try to create user and mount namespaces
506+
bool try_unshare(uid_t uid, gid_t gid, const char* unshare_uid, const char* unshare_gid, bool verbose) {
507+
uid_t target_uid = uid;
508+
gid_t target_gid = gid;
509+
510+
if (unshare_uid != NULL && unshare_uid[0] != '\0') {
511+
char* endptr;
512+
long val = strtol(unshare_uid, &endptr, 10);
513+
if (endptr != unshare_uid && *endptr == '\0' && val >= 0) {
514+
target_uid = (uid_t)val;
515+
}
516+
}
517+
518+
if (unshare_gid != NULL && unshare_gid[0] != '\0') {
519+
char* endptr;
520+
long val = strtol(unshare_gid, &endptr, 10);
521+
if (endptr != unshare_gid && *endptr == '\0' && val >= 0) {
522+
target_gid = (gid_t)val;
523+
}
524+
}
525+
526+
int flags = CLONE_NEWUSER | CLONE_NEWNS;
527+
if (unshare(flags) == 0) {
528+
// Disable setgroups
529+
FILE* f = fopen("/proc/self/setgroups", "w");
530+
if (f != NULL) {
531+
fputs("deny", f);
532+
fclose(f);
533+
}
534+
535+
// Write uid_map
536+
char uid_map[64];
537+
snprintf(uid_map, sizeof(uid_map), "%u %u 1", target_uid, uid);
538+
f = fopen("/proc/self/uid_map", "w");
539+
if (f == NULL) {
540+
fprintf(stderr, "Failed to open /proc/self/uid_map: %s\n", strerror(errno));
541+
return false;
542+
}
543+
if (fputs(uid_map, f) == EOF) {
544+
fprintf(stderr, "Failed to write uid_map: %s\n", strerror(errno));
545+
fclose(f);
546+
return false;
547+
}
548+
fclose(f);
549+
550+
// Write gid_map
551+
char gid_map[64];
552+
snprintf(gid_map, sizeof(gid_map), "%u %u 1", target_gid, gid);
553+
f = fopen("/proc/self/gid_map", "w");
554+
if (f == NULL) {
555+
fprintf(stderr, "Failed to open /proc/self/gid_map: %s\n", strerror(errno));
556+
return false;
557+
}
558+
if (fputs(gid_map, f) == EOF) {
559+
fprintf(stderr, "Failed to write gid_map: %s\n", strerror(errno));
560+
fclose(f);
561+
return false;
562+
}
563+
fclose(f);
564+
565+
restore_capabilities();
566+
567+
if (!try_make_mount_private()) {
568+
fprintf(stderr, "Warning: failed to make mount private: %s\n", strerror(errno));
569+
}
570+
571+
if (verbose) {
572+
fprintf(stderr, "Successfully created user and mount namespaces\n");
573+
}
574+
575+
return true;
576+
}
577+
578+
fprintf(stderr, "Failed to create user and mount namespaces: %s\n", strerror(errno));
579+
return false;
580+
}
581+
582+
// Check if a binary is setuid root and executable
583+
bool is_suid_exe(const char* path) {
584+
struct stat sb;
585+
if (stat(path, &sb) == -1) {
586+
return false;
587+
}
588+
// Check if owned by root, has SUID bit, and is executable
589+
return (sb.st_uid == 0 && (sb.st_mode & S_ISUID) != 0 && (sb.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0);
590+
}
591+
417592
char* find_fusermount(bool verbose) {
418593
char* fusermount_base = "fusermount";
419594

@@ -449,16 +624,9 @@ char* find_fusermount(bool verbose) {
449624
sprintf(fusermount_full_path, "%s/%s", dir, entry->d_name);
450625

451626
// Check if the binary is setuid root
452-
struct stat sb;
453-
if (stat(fusermount_full_path, &sb) == -1) {
454-
perror("stat");
455-
free(fusermount_full_path);
456-
continue;
457-
}
458-
459-
if (sb.st_uid != 0 || (sb.st_mode & S_ISUID) == 0) {
627+
if (!is_suid_exe(fusermount_full_path)) {
460628
if (verbose) {
461-
printf("Not setuid root, skipping...\n");
629+
printf("Not setuid root executable, skipping...\n");
462630
}
463631
free(fusermount_full_path);
464632
continue;
@@ -514,6 +682,38 @@ char* find_fusermount(bool verbose) {
514682
return NULL;
515683
}
516684

685+
// Check FUSE availability and attempt unshare if needed
686+
bool check_fuse(bool verbose, uid_t uid, gid_t gid, const char* unshare_uid_str, const char* unshare_gid_str, bool* unshare_succeeded) {
687+
// First check if /dev/fuse is accessible
688+
if (access("/dev/fuse", R_OK) != 0 || access("/dev/fuse", W_OK) != 0) {
689+
return false;
690+
}
691+
692+
// If we're root or already in a namespace, we're good
693+
if (uid == 0 || *unshare_succeeded || is_in_user_and_mount_namespace()) {
694+
return true;
695+
}
696+
697+
// Check if we have a SUID fusermount
698+
char* fusermount = find_fusermount(verbose);
699+
if (fusermount != NULL) {
700+
free(fusermount);
701+
return true;
702+
}
703+
704+
// No SUID fusermount found, try unshare
705+
if (verbose) {
706+
fprintf(stderr, "SUID fusermount not found in PATH, trying to unshare...\n");
707+
}
708+
709+
if (try_unshare(uid, gid, unshare_uid_str, unshare_gid_str, verbose)) {
710+
*unshare_succeeded = true;
711+
return true;
712+
}
713+
714+
return true; // Return true anyway to let FUSE try
715+
}
716+
517717
/* Exit status to use when launching an AppImage fails.
518718
* For applications that assign meanings to exit status codes (e.g. rsync),
519719
* we avoid "cluttering" pre-defined exit status codes by using 127 which
@@ -681,9 +881,18 @@ void print_help(const char* appimage_path) {
681881
" --appimage-portable-config Create a portable config folder to use as\n"
682882
" $XDG_CONFIG_HOME\n"
683883
" --appimage-signature Print digital signature embedded in AppImage\n"
884+
" --appimage-unshare Try to use unshare user and mount namespaces\n"
684885
" --appimage-updateinfo[rmation] Print update info embedded in AppImage\n"
685886
" --appimage-version Print version of AppImage runtime\n"
686887
"\n"
888+
"Environment variables:\n"
889+
"\n"
890+
" APPIMAGE_EXTRACT_AND_RUN=1 Temporarily extract and run without FUSE\n"
891+
" APPIMAGE_UNSHARE=1 Try to use unshare user and mount namespaces\n"
892+
" APPIMAGE_UNSHARE_ROOT=1 Map to root (UID 0, GID 0) in user namespace\n"
893+
" APPIMAGE_UNSHARE_UID=<uid> Map to specified UID in user namespace\n"
894+
" APPIMAGE_UNSHARE_GID=<gid> Map to specified GID in user namespace\n"
895+
"\n"
687896
"Portable home:\n"
688897
"\n"
689898
" If you would like the application contained inside this AppImage to store its\n"
@@ -1698,13 +1907,64 @@ int main(int argc, char* argv[]) {
16981907
portable_option(arg, appimage_path, "home");
16991908
portable_option(arg, appimage_path, "config");
17001909

1910+
// Check for --appimage-unshare flag
1911+
bool requested_unshare = false;
1912+
if (arg && strcmp(arg, "appimage-unshare") == 0) {
1913+
requested_unshare = true;
1914+
}
1915+
17011916
// If there is an argument starting with appimage- (but not appimage-mount which is handled further down)
17021917
// then stop here and print an error message
1703-
if ((arg && strncmp(arg, "appimage-", 8) == 0) && (arg && strcmp(arg, "appimage-mount") != 0)) {
1918+
if ((arg && strncmp(arg, "appimage-", 8) == 0) &&
1919+
(arg && strcmp(arg, "appimage-mount") != 0) &&
1920+
(arg && strcmp(arg, "appimage-unshare") != 0)) {
17041921
fprintf(stderr, "--%s is not yet implemented in version %s\n", arg, GIT_COMMIT);
17051922
exit(1);
17061923
}
17071924

1925+
// Get UID and GID for unshare
1926+
uid_t uid = getuid();
1927+
gid_t gid = getgid();
1928+
1929+
// Check environment variables for unshare
1930+
const char* unshare_env = getenv("APPIMAGE_UNSHARE");
1931+
const char* unshare_root = getenv("APPIMAGE_UNSHARE_ROOT");
1932+
const char* unshare_uid_env = getenv("APPIMAGE_UNSHARE_UID");
1933+
const char* unshare_gid_env = getenv("APPIMAGE_UNSHARE_GID");
1934+
1935+
// Determine if we should attempt unshare
1936+
bool should_unshare = requested_unshare ||
1937+
(unshare_env != NULL && strcmp(unshare_env, "1") == 0) ||
1938+
(unshare_root != NULL && strcmp(unshare_root, "1") == 0) ||
1939+
(unshare_uid_env != NULL && unshare_uid_env[0] != '\0') ||
1940+
(unshare_gid_env != NULL && unshare_gid_env[0] != '\0');
1941+
1942+
// If APPIMAGE_UNSHARE_ROOT is set, map to root
1943+
const char* target_uid_str = unshare_uid_env;
1944+
const char* target_gid_str = unshare_gid_env;
1945+
if (unshare_root != NULL && strcmp(unshare_root, "1") == 0) {
1946+
target_uid_str = "0";
1947+
target_gid_str = "0";
1948+
}
1949+
1950+
bool unshare_succeeded = false;
1951+
1952+
// Attempt unshare if requested
1953+
if (should_unshare) {
1954+
unshare_succeeded = try_unshare(uid, gid, target_uid_str, target_gid_str, verbose);
1955+
}
1956+
1957+
// Check FUSE availability and attempt unshare if needed
1958+
if (!check_fuse(verbose, uid, gid, target_uid_str, target_gid_str, &unshare_succeeded)) {
1959+
fprintf(stderr, "FUSE is not available\n");
1960+
exit(EXIT_EXECERROR);
1961+
}
1962+
1963+
// Restore capabilities if we unshared successfully
1964+
if (unshare_succeeded) {
1965+
restore_capabilities();
1966+
}
1967+
17081968
int dir_fd, res;
17091969

17101970
size_t templen = strlen(temp_base);

0 commit comments

Comments
 (0)