323 lines
8.8 KiB
C
323 lines
8.8 KiB
C
/*
|
|
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 <fcntl.h>
|
|
#include <pthread.h>
|
|
#include <sqlite3.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/fsuid.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
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 ) ORDER BY LENGTH( filename ) DESC;";
|
|
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 && step_ret != SQLITE_DONE) {
|
|
fprintf(stderr, "SQLite error: %s\n", sqlite3_errstr(step_ret));
|
|
sqlite3_finalize(stmt);
|
|
return ret;
|
|
}
|
|
if (step_ret == SQLITE_ROW) {
|
|
int mode_col = sqlite3_column_int(stmt, 0);
|
|
if (mode_col) {
|
|
ret = ALLOW;
|
|
} else {
|
|
ret = DENY;
|
|
}
|
|
}
|
|
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;
|
|
}
|