/* ICFS: Interactively Controlled File System Copyright (C) 2024-2025 Fedir Kovalov This program can be distributed under the terms of the GNU GPLv2. See the file LICENSE. */ #include "perm_permissions_table.h" #include "access_t.h" #include "proc_operations.h" #include "process_info.h" #include "set_mode_t.h" #include #include #include #include #include #include #include #include #include #include #include sqlite3 *perm_database = NULL; const char *const table_name = "permissions"; // one row corresponds to a permission to access one file for one executable const int column_count = 3; const char *const schema[] = {"executable", "filename", "mode"}; const char *const types[] = {"TEXT", "TEXT", "INTEGER"}; uid_t ruid, euid, current_uid; pthread_mutex_t uid_switch = PTHREAD_MUTEX_INITIALIZER; void set_db_fsuid() { pthread_mutex_lock(&uid_switch); if (current_uid == ruid) return; int status = -1; status = setfsuid(ruid); if (status < 0) { fprintf(stderr, "Couldn't set uid to %d.\n", ruid); exit(status); } pthread_mutex_unlock(&uid_switch); } void set_real_fsuid() { pthread_mutex_lock(&uid_switch); if (current_uid == ruid) return; int status = -1; status = setfsuid(ruid); if (status < 0) { fprintf(stderr, "Couldn't set uid to %d.\n", euid); exit(status); } pthread_mutex_unlock(&uid_switch); } static int check_table_col_schema(void *notused, int argc, char **argv, char **colname) { (void)notused; (void)colname; if (argc < 3) { fprintf(stderr, "Unexpected amount of arguments given to the callback.\n"); return 1; } int column_num = atoi(argv[0]); if (column_num >= column_count) { fprintf(stderr, "Table contains unexpected amount of columns.\n"); return 1; } if (strcmp(schema[column_num], argv[1]) == 0 && strcmp(types[column_num], argv[2]) == 0) { return 0; } fprintf(stderr, "Column %d does not conform to the schema.\n", column_num); return 1; } static int set_flag(void *flag, int argc, char **argv, char **colname) { (void)colname; if (argc < 3) { fprintf(stderr, "Unexpected amount of arguments given to the callback: %d.\n", argc); return 1; } if (atoi(argv[2])) { fprintf(stderr, "Third column was: %s\n", argv[2]); *(int *)flag = 1; } else { *(int *)flag = -1; } return 0; } int create_database_schema() { fprintf(stderr, "Creating table 'permissions'.\n"); const char *create_query = "CREATE TABLE permissions(executable TEXT NOT " "NULL, filename TEXT NOT NULL, mode INTEGER NOT NULL);"; char *err = NULL; int ret = sqlite3_exec(perm_database, create_query, NULL, NULL, &err); if (ret != SQLITE_OK) { fprintf(stderr, "sqlite3 error: %s\n", err); sqlite3_free(err); return 1; } fprintf(stderr, "Database created successfully\n"); return 0; } /** * Ensures that the database schema is correct. * * @return: 0 if the schema is correct, 1 if the schema could not be corrected. */ int ensure_database_schema() { // Check for the table. int result = sqlite3_table_column_metadata( perm_database, NULL, table_name, NULL, NULL, NULL, NULL, NULL, NULL); if (result == SQLITE_ERROR) { fprintf(stderr, "Table '%s' does not exist.\n", table_name); if (create_database_schema()) { fprintf(stderr, "Table could not be created.\n"); return 1; } return 0; } else if (result != SQLITE_OK) { fprintf(stderr, "Database metadata could not be retrieved.\n"); return 1; } const char *pragma = "PRAGMA table_info(permissions);"; char *err = NULL; int ret = sqlite3_exec(perm_database, pragma, check_table_col_schema, NULL, &err); if (ret != SQLITE_OK) { fprintf(stderr, "sqlite3 error: %s\n", err); sqlite3_free(err); return 1; } fprintf(stderr, "Schema is correct.\n"); return 0; } /** * Initializes the permanent permissions table. * * @param db_filename: The filename of the permissions sqlite3 database * @return: 0 on success, -1 on failure */ int init_perm_permissions_table(const char *db_filename) { // we don't want the group and others to access the db umask(0077); ruid = getuid(); euid = geteuid(); fprintf(stderr, "Running with uid: %d, gid: %d\n", euid, getegid()); if (sqlite3_open_v2(db_filename, &perm_database, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL)) { perror("Can't open permanent permissions database"); return -1; } umask(0); if (ensure_database_schema()) { fprintf(stderr, "Database schema is not correct.\n"); return -1; } int status = seteuid(ruid); if (status < 0) { fprintf(stderr, "Couldn't set euid to ruid during database setup.\n"); exit(status); } return 0; } /** * Destroys the permanent permissions table. */ void destroy_perm_permissions_table(void) { sqlite3_close(perm_database); } /** * Checks if the process has a permanent access to the file. * * @param filename: The file that the process is trying to access * @param pi: The process information * @return: access status - ALLOW, DENY or NDEF in case if no information was * found */ access_t check_perm_access_noparent(const char *filename, struct process_info pi) { if (pi.name == NULL) return NDEF; access_t ret = NDEF; sqlite3_stmt *stmt = NULL; const char *sql = "SELECT mode FROM permissions WHERE executable = ?1 " "AND (( ?2 LIKE CONCAT(filename, \'%\') AND filename " "GLOB \'*/\') OR filename = ?2 );"; sqlite3_prepare_v2(perm_database, sql, -1, &stmt, NULL); sqlite3_bind_text(stmt, 1, pi.name, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, filename, -1, SQLITE_STATIC); int step_ret = sqlite3_step(stmt); if (step_ret == SQLITE_ROW) { int mode_col = sqlite3_column_int(stmt, 0); if (mode_col) { ret = ALLOW; } else { ret = DENY; } } else { fprintf(stderr, "SQLite error: %s\n", sqlite3_errstr(step_ret)); } sqlite3_finalize(stmt); return ret; } /** * Checks if the process or any of it's parents have permanent access to the * file. * * @param filename: The file that the process is trying to access * @param pi: The process information * @return: access status - ALLOW, DENY or NDEF in case if no information was * found. Does not return ALLOW_TEMP or DENY_TEMP. * @note: In case one of the parent processes is killed while this function * execution the result is not guranteed to be correct. It should only lead to * false negatives, though. */ access_t check_perm_access(const char *filename, struct process_info pi) { if (pi.PID == 0 || pi.name == NULL) { return NDEF; } struct process_info current_pi = pi; current_pi.name = strdup(current_pi.name); while (current_pi.PID != 0) { access_t access = check_perm_access_noparent(filename, current_pi); free(current_pi.name); if (access != NDEF) { return access; } current_pi.name = NULL; while (current_pi.name == NULL) { current_pi.PID = get_main_thread_pid(get_parent_pid(current_pi.PID)); if (current_pi.PID != 0) { current_pi.name = get_process_name_by_pid(current_pi.PID); } else { break; } } } return NDEF; } /** * Gives permanent access to the process to the file. * * @param filename: The file that the process is trying to access * @param pi: The process information * @return: 0 on success, 1 on failure */ int set_perm_access(const char *filename, struct process_info pi, set_mode_t mode) { char *query = NULL; int ret = -1; if (mode == SET_ALLOW) { ret = asprintf(&query, "INSERT INTO %s VALUES (\'%s\', \'%s\', TRUE);", table_name, pi.name, filename); } else if (mode == SET_DENY) { ret = asprintf(&query, "INSERT INTO %s VALUES (\'%s\', \'%s\', FALSE);", table_name, pi.name, filename); } else { return 1; } if (ret < 0) { // If asprintf fails, the contents of query are undefined (see man // asprintf). That does not explicitly rule out that query will be a valid // pointer. But the risk of freeing a non-allocated pointer is too much to // justify preparing for this. fprintf(stderr, "Could not create query on rule insertion\n"); perror(""); return 1; } char *sqlite_error = NULL; ret = sqlite3_exec(perm_database, query, NULL, NULL, &sqlite_error); free(query); if (ret != SQLITE_OK) { fprintf(stderr, "SQLite returned an error: %s\n", sqlite_error); sqlite3_free(sqlite_error); return 1; } return 0; }