Skip to content

Commit a9a2535

Browse files
fix(cli): trust root-owned per-user /home entries during activation walk
Managed Linux hosts keep /home a real directory and point /home/<user> at another tree (e.g. /local/home/<user>). The O_NOFOLLOW walk rejected that entry and install failed with 'activation transaction I/O failed'. Resolve /home/<name> only when /home is root-owned and not group/world writable, the entry is a root-owned symlink, and it resolves to a directory owned by root or the current user. Arbitrary user-owned symlinks are still rejected. Fixes #2306 Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: BumaldaOverTheWater94 <83429948+BumaldaOverTheWater94@users.noreply.github.com>
1 parent 5958f54 commit a9a2535

1 file changed

Lines changed: 53 additions & 0 deletions

File tree

‎src/cli/activation_transaction.c‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,53 @@ static bool activation_posix_acl_empty(int descriptor) {
889889
return cbm_macos_extended_acl_fd_is_empty(descriptor);
890890
}
891891

892+
#ifdef __linux__
893+
/* Managed Linux hosts often keep /home a real directory and point the per-user
894+
* entry elsewhere (/home/alice -> /local/home/alice). Trust that entry only
895+
* when root owns it inside a /home nobody else can write, and it resolves to a
896+
* directory owned by root or the current user. Returns NULL when not
897+
* applicable; the O_NOFOLLOW walk then rejects the path as before. */
898+
static char *activation_linux_home_entry_path(const char *directory) {
899+
static const char home_prefix[] = "/home/";
900+
size_t prefix_length = sizeof(home_prefix) - 1U;
901+
if (strncmp(directory, home_prefix, prefix_length) != 0) {
902+
return NULL;
903+
}
904+
size_t name_length = strcspn(directory + prefix_length, "/");
905+
char entry[sizeof(home_prefix) + 256U];
906+
if (name_length == 0 || name_length >= 256U) {
907+
return NULL;
908+
}
909+
memcpy(entry, directory, prefix_length + name_length);
910+
entry[prefix_length + name_length] = '\0';
911+
912+
struct stat home_status;
913+
struct stat entry_status;
914+
struct stat resolved_status;
915+
char resolved[4096];
916+
if (lstat("/home", &home_status) != 0 || !S_ISDIR(home_status.st_mode) ||
917+
home_status.st_uid != 0 || (home_status.st_mode & 0022) != 0 ||
918+
lstat(entry, &entry_status) != 0 || !S_ISLNK(entry_status.st_mode) ||
919+
entry_status.st_uid != 0 || !realpath(entry, resolved) ||
920+
lstat(resolved, &resolved_status) != 0 || !S_ISDIR(resolved_status.st_mode) ||
921+
(resolved_status.st_uid != 0 && resolved_status.st_uid != geteuid())) {
922+
return NULL;
923+
}
924+
const char *rest = directory + prefix_length + name_length;
925+
size_t needed = strlen(resolved) + strlen(rest) + 1U;
926+
char *mapped = malloc(needed);
927+
if (!mapped) {
928+
return NULL;
929+
}
930+
int written = snprintf(mapped, needed, "%s%s", resolved, rest);
931+
if (written <= 0 || (size_t)written >= needed) {
932+
free(mapped);
933+
return NULL;
934+
}
935+
return mapped;
936+
}
937+
#endif
938+
892939
static char *activation_posix_walk_path(const char *directory) {
893940
#if defined(__APPLE__) || defined(__linux__)
894941
/* macOS and immutable Linux layouts can expose writable trees through
@@ -935,6 +982,12 @@ static char *activation_posix_walk_path(const char *directory) {
935982
}
936983
return mapped;
937984
}
985+
#endif
986+
#ifdef __linux__
987+
char *home_entry = activation_linux_home_entry_path(directory);
988+
if (home_entry) {
989+
return home_entry;
990+
}
938991
#endif
939992
return activation_string_copy(directory);
940993
}

0 commit comments

Comments
 (0)