One classification serves both the resolver and the diagnostic, so the * reason a user reads is exactly the rule that refused the link. On REFUSED * every descriptor opened along the way is closed again. */
| 201 | * reason a user reads is exactly the rule that refused the link. On REFUSED |
| 202 | * every descriptor opened along the way is closed again. */ |
| 203 | static int edit_path_classify(const char *path, cbm_config_edit_target_t *target, char *reason, |
| 204 | size_t reason_size) { |
| 205 | edit_path_target_reset(target); |
| 206 | if (reason && reason_size > 0U) { |
| 207 | reason[0] = '\0'; |
| 208 | } |
| 209 | if (!path || !path[0]) { |
| 210 | return CBM_CONFIG_EDIT_PATH_REFUSED; |
| 211 | } |
| 212 | struct stat link_state; |
| 213 | if (lstat(path, &link_state) != 0 || !S_ISLNK(link_state.st_mode)) { |
| 214 | return edit_path_copy(path, target->path, sizeof(target->path)) == 0 |
| 215 | ? CBM_CONFIG_EDIT_PATH_DIRECT |
| 216 | : CBM_CONFIG_EDIT_PATH_REFUSED; |
| 217 | } |
| 218 | if (!edit_path_link_dir_under_root(path)) { |
| 219 | edit_path_note(reason, reason_size, |
| 220 | edit_path_root_count == 0U |
| 221 | ? "symlinks are not followed by this command" |
| 222 | : "symlink outside the opted-in configuration roots"); |
| 223 | return CBM_CONFIG_EDIT_PATH_REFUSED; |
| 224 | } |
| 225 | uid_t self = edit_path_invoking_uid(); |
| 226 | if (self == 0 && link_state.st_uid != 0) { |
| 227 | if (reason && reason_size > 0U) { |
| 228 | (void)snprintf(reason, reason_size, |
| 229 | "symlink owned by uid %lu is not followed by a root process", |
| 230 | (unsigned long)link_state.st_uid); |
| 231 | } |
| 232 | return CBM_CONFIG_EDIT_PATH_REFUSED; |
| 233 | } |
| 234 | if (link_state.st_uid != self) { |
| 235 | edit_path_note_owner(reason, reason_size, "symlink", (unsigned long)link_state.st_uid, |
| 236 | (unsigned long)self); |
| 237 | return CBM_CONFIG_EDIT_PATH_REFUSED; |
| 238 | } |
| 239 | char resolved[CBM_CONFIG_EDIT_PATH_MAX]; |
| 240 | struct stat target_state; |
| 241 | if (!cbm_canonical_path(path, resolved, sizeof(resolved)) || |
| 242 | stat(resolved, &target_state) != 0) { |
| 243 | edit_path_note(reason, reason_size, "dangling symlink"); |
| 244 | return CBM_CONFIG_EDIT_PATH_REFUSED; |
| 245 | } |
| 246 | if (!S_ISREG(target_state.st_mode)) { |
| 247 | edit_path_note(reason, reason_size, |
| 248 | S_ISDIR(target_state.st_mode) ? "symlink to a directory" |
| 249 | : "symlink to a special file"); |
| 250 | return CBM_CONFIG_EDIT_PATH_REFUSED; |
| 251 | } |
| 252 | if (target_state.st_uid != self) { |
| 253 | edit_path_note_owner(reason, reason_size, "symlink target", |
| 254 | (unsigned long)target_state.st_uid, (unsigned long)self); |
| 255 | return CBM_CONFIG_EDIT_PATH_REFUSED; |
| 256 | } |
| 257 | #if CBM_CONFIG_EDIT_TARGET_UNDER_ROOT |
| 258 | if (!edit_path_under_root(resolved)) { |
| 259 | edit_path_note(reason, reason_size, "symlink target outside the configuration roots"); |
| 260 | return CBM_CONFIG_EDIT_PATH_REFUSED; |
no test coverage detected