[io, test] Added more file io tests

This commit is contained in:
Martin Fouilleul 2023-06-16 11:57:53 +02:00
parent ec53cd5416
commit a64c86ca4e
14 changed files with 438 additions and 171 deletions

View File

@ -140,6 +140,9 @@ MP_API io_cmp io_wait_single_req(io_req* req);
// File IO wrapper API
//----------------------------------------------------------------
MP_API file_handle file_handle_nil();
MP_API bool file_handle_is_nil(file_handle handle);
MP_API file_handle file_open(str8 path, file_access_rights rights, file_open_flags flags);
MP_API file_handle file_open_at(file_handle dir, str8 path, file_access_rights rights, file_open_flags flags);
MP_API void file_close(file_handle file);

View File

@ -11,6 +11,16 @@
// File stream read/write API
//------------------------------------------------------------------------------
file_handle file_handle_nil()
{
return((file_handle){0});
}
bool file_handle_is_nil(file_handle handle)
{
return(handle.h == 0);
}
file_handle file_open(str8 path, file_access_rights rights, file_open_flags flags)
{
io_req req = {.op = IO_OP_OPEN_AT,

View File

@ -117,124 +117,134 @@ io_open_restrict_result io_open_restrict(io_file_desc dirFd, str8 path, file_acc
.fd = dirFd,
};
file_status status;
context.error = io_raw_fstat(dirFd, &status);
context.rootUID = status.uid;
for_list(&pathElements.list, elt, str8_elt, listElt)
if(io_file_desc_is_nil(dirFd))
{
str8 name = elt->string;
file_access_rights eltAccessRights = FILE_ACCESS_READ;
file_open_flags eltOpenFlags = 0;
context.error = IO_ERR_HANDLE;
}
else
{
file_status status;
context.error = io_raw_fstat(dirFd, &status);
context.rootUID = status.uid;
}
bool atLastElement = (&elt->listElt == list_last(&pathElements.list));
if(atLastElement)
if(context.error == IO_OK)
{
for_list(&pathElements.list, elt, str8_elt, listElt)
{
eltAccessRights = accessRights;
eltOpenFlags = openFlags;
}
str8 name = elt->string;
file_access_rights eltAccessRights = FILE_ACCESS_READ;
file_open_flags eltOpenFlags = 0;
if( !str8_cmp(name, STR8("."))
&& !atLastElement)
{
//NOTE: if we're not at the last element we can just skip '.' elements
continue;
}
else if(!str8_cmp(name, STR8("..")))
{
//NOTE: check that we don't escape root dir
file_status status;
context.error = io_raw_fstat(context.fd, &status);
if(context.error)
bool atLastElement = (&elt->listElt == list_last(&pathElements.list));
if(atLastElement)
{
break;
}
else if(status.uid == context.rootUID)
{
context.error = IO_ERR_WALKOUT;
break;
}
}
else if(!io_raw_file_exists_at(context.fd, name, FILE_OPEN_SYMLINK))
{
//NOTE: if the file doesn't exists, but we're at the last element and FILE_OPEN_CREATE
// is set, we create the file. Otherwise it is a IO_ERROR_NO_ENTRY error.
if( !atLastElement
|| !(openFlags & FILE_OPEN_CREATE))
{
context.error = IO_ERR_NO_ENTRY;
break;
}
}
else
{
//NOTE: if the file exists, we check the type of file
file_status status = {0};
context.error = io_raw_fstat_at(context.fd, name, FILE_OPEN_SYMLINK, &status);
if(context.error)
{
break;
eltAccessRights = accessRights;
eltOpenFlags = openFlags;
}
if(status.type == MP_FILE_REGULAR)
if( !str8_cmp(name, STR8("."))
&& !atLastElement)
{
if(!atLastElement)
//NOTE: if we're not at the last element we can just skip '.' elements
continue;
}
else if(!str8_cmp(name, STR8("..")))
{
//NOTE: check that we don't escape root dir
file_status status;
context.error = io_raw_fstat(context.fd, &status);
if(context.error)
{
break;
}
else if(status.uid == context.rootUID)
{
context.error = IO_ERR_WALKOUT;
break;
}
}
else if(!io_raw_file_exists_at(context.fd, name, FILE_OPEN_SYMLINK))
{
//NOTE: if the file doesn't exists, but we're at the last element and FILE_OPEN_CREATE
// is set, we create the file. Otherwise it is a IO_ERROR_NO_ENTRY error.
if( !atLastElement
|| !(openFlags & FILE_OPEN_CREATE))
{
context.error = IO_ERR_NO_ENTRY;
break;
}
}
else
{
//NOTE: if the file exists, we check the type of file
file_status status = {0};
context.error = io_raw_fstat_at(context.fd, name, FILE_OPEN_SYMLINK, &status);
if(context.error)
{
break;
}
if(status.type == MP_FILE_REGULAR)
{
if(!atLastElement)
{
context.error = IO_ERR_NOT_DIR;
break;
}
}
else if(status.type == MP_FILE_SYMLINK)
{
//TODO - do we need a FILE_OPEN_NO_FOLLOW that fails if last element is a symlink?
// - do we need a FILE_OPEN_NO_SYMLINKS that fails if _any_ element is a symlink?
if( !atLastElement
|| !(openFlags & FILE_OPEN_SYMLINK))
{
io_raw_read_link_result link = io_raw_read_link_at(scratch.arena, context.fd, name);
if(link.error)
{
context.error = link.error;
break;
}
if(link.target.len == 0)
{
//NOTE: treat an empty target as a '.'
link.target = STR8(".");
}
else if(path_is_absolute(link.target))
{
context.error = IO_ERR_WALKOUT;
break;
}
str8_list linkElements = str8_split(scratch.arena, link.target, sep);
if(!list_empty(&linkElements.list))
{
//NOTE: insert linkElements into pathElements after elt
list_elt* tmp = elt->listElt.next;
elt->listElt.next = linkElements.list.first;
linkElements.list.last->next = tmp;
if(!tmp)
{
pathElements.list.last = linkElements.list.last;
}
}
continue;
}
}
else if(status.type != MP_FILE_DIRECTORY)
{
context.error = IO_ERR_NOT_DIR;
break;
}
}
else if(status.type == MP_FILE_SYMLINK)
{
//TODO - do we need a FILE_OPEN_NO_FOLLOW that fails if last element is a symlink?
// - do we need a FILE_OPEN_NO_SYMLINKS that fails if _any_ element is a symlink?
if( !atLastElement
|| !(openFlags & FILE_OPEN_SYMLINK))
{
io_raw_read_link_result link = io_raw_read_link_at(scratch.arena, context.fd, name);
if(link.error)
{
context.error = link.error;
break;
}
if(link.target.len == 0)
{
//NOTE: treat an empty target as a '.'
link.target = STR8(".");
}
else if(path_is_absolute(link.target))
{
context.error = IO_ERR_WALKOUT;
break;
}
str8_list linkElements = str8_split(scratch.arena, link.target, sep);
if(!list_empty(&linkElements.list))
{
//NOTE: insert linkElements into pathElements after elt
list_elt* tmp = elt->listElt.next;
elt->listElt.next = linkElements.list.first;
linkElements.list.last->next = tmp;
if(!tmp)
{
pathElements.list.last = linkElements.list.last;
}
}
continue;
}
}
else if(status.type != MP_FILE_DIRECTORY)
{
context.error = IO_ERR_NOT_DIR;
break;
}
//NOTE: if we arrive here, we have no errors and the correct flags are set,
// so we can enter the element
DEBUG_ASSERT(context.error == IO_OK);
io_open_restrict_enter(&context, name, eltAccessRights, eltOpenFlags);
}
//NOTE: if we arrive here, we have no errors and the correct flags are set,
// so we can enter the element
DEBUG_ASSERT(context.error == IO_OK);
io_open_restrict_enter(&context, name, eltAccessRights, eltOpenFlags);
}
if(context.error && !io_file_desc_is_nil(context.fd))
@ -270,38 +280,50 @@ io_cmp io_open_at(file_slot* atSlot, io_req* req, file_table* table)
slot->fd = -1;
cmp.handle = file_handle_from_slot(table, slot);
slot->rights = req->open.rights;
if(atSlot)
{
slot->rights &= atSlot->rights;
}
if(slot->rights != req->open.rights)
str8 path = str8_from_buffer(req->size, req->buffer);
if(!path.len)
{
slot->error = IO_ERR_PERM;
slot->fatal = true;
slot->error = IO_ERR_ARG;
}
else if(!atSlot && !file_handle_is_nil(req->handle))
{
slot->error = IO_ERR_HANDLE;
}
else
{
str8 path = str8_from_buffer(req->size, req->buffer);
io_file_desc dirFd = atSlot ? atSlot->fd : io_file_desc_nil();
if(req->open.flags & FILE_OPEN_RESTRICT)
slot->rights = req->open.rights;
if(atSlot)
{
io_open_restrict_result res = io_open_restrict(dirFd, path, slot->rights, req->open.flags);
slot->error = res.error;
slot->fd = res.fd;
slot->rights &= atSlot->rights;
}
if(slot->rights != req->open.rights)
{
slot->error = IO_ERR_PERM;
}
else
{
slot->fd = io_raw_open_at(dirFd, path, slot->rights, req->open.flags);
if(io_file_desc_is_nil(slot->fd))
io_file_desc dirFd = atSlot ? atSlot->fd : io_file_desc_nil();
if(req->open.flags & FILE_OPEN_RESTRICT)
{
slot->error = io_raw_last_error();
io_open_restrict_result res = io_open_restrict(dirFd, path, slot->rights, req->open.flags);
slot->error = res.error;
slot->fd = res.fd;
}
else
{
slot->fd = io_raw_open_at(dirFd, path, slot->rights, req->open.flags);
if(io_file_desc_is_nil(slot->fd))
{
slot->error = io_raw_last_error();
}
}
}
}
if(slot->error)
{
slot->fatal = true;

View File

@ -0,0 +1 @@
Hello from directory/test.txt

View File

@ -0,0 +1 @@
Hello from jail/dir/test.txt

View File

@ -0,0 +1 @@
../regular.txt

View File

@ -0,0 +1 @@
Hello from jail/test.txt

View File

@ -1 +1 @@
Hello, world!
Hello from regular.txt

View File

@ -1 +0,0 @@
regular.txt

View File

@ -9,10 +9,15 @@
#include<stdio.h>
#include"milepost.h"
int test_write(mem_arena* arena, str8 path, str8 test_string)
int test_write()
{
log_info("writing\n");
mem_arena* arena = mem_scratch();
str8 path = STR8("./data/write_test.txt");
str8 test_string = STR8("Hello from write_test.txt");
file_handle f = file_open(path, FILE_ACCESS_WRITE, FILE_OPEN_CREATE|FILE_OPEN_TRUNCATE);
if(file_last_error(f))
{
@ -48,10 +53,31 @@ int test_write(mem_arena* arena, str8 path, str8 test_string)
return(0);
}
int test_read(mem_arena* arena, str8 path, str8 test_string)
int check_string(file_handle f, str8 test_string)
{
char buffer[256];
i64 n = file_read(f, 256, buffer);
if(file_last_error(f))
{
log_error("Error while reading test string\n");
return(-1);
}
if(str8_cmp(test_string, str8_from_buffer(n, buffer)))
{
return(-1);
}
return(0);
}
int test_read()
{
log_info("reading\n");
str8 path = STR8("./data/regular.txt");
str8 test_string = STR8("Hello from regular.txt");
file_handle f = file_open(path, FILE_ACCESS_READ, 0);
if(file_last_error(f))
{
@ -59,28 +85,24 @@ int test_read(mem_arena* arena, str8 path, str8 test_string)
return(-1);
}
char buffer[256];
i64 n = file_read(f, 256, buffer);
if(file_last_error(f))
if(check_string(f, test_string))
{
log_error("Error while reading %.*s\n", (int)path.len, path.ptr);
log_error("Check string failed\n");
return(-1);
}
file_close(f);
if(str8_cmp(test_string, str8_from_buffer(n, buffer)))
{
log_error("Didn't recover test string\n");
return(-1);
}
return(0);
}
int test_stat_size(str8 path, u64 size)
int test_stat_size()
{
log_info("stat size\n");
str8 path = STR8("./data/regular.txt");
str8 test_string = STR8("Hello from regular.txt");
u64 size = test_string.len;
file_handle f = file_open(path, 0, 0);
if(file_last_error(f))
{
@ -106,16 +128,11 @@ int test_stat_size(str8 path, u64 size)
return(0);
}
int test_stat_type(mem_arena* arena, str8 dataDir)
int test_stat_type()
{
str8 regular = path_append(arena, dataDir, STR8("regular.txt"));
str8 dir = path_append(arena, dataDir, STR8("directory"));
#if PLATFORM_WINDOWS
str8 link = path_append(arena, dataDir, STR8("win32_symlink"));
#else
str8 link = path_append(arena, dataDir, STR8("posix_symlink"));
#endif
str8 regular = STR8("./data/regular.txt");
str8 dir = STR8("./data/directory");
str8 link = STR8("./data/symlink");
log_info("stat type, regular\n");
@ -186,6 +203,120 @@ int test_stat_type(mem_arena* arena, str8 dataDir)
return(0);
}
int test_symlinks()
{
// open symlink target
log_info("open symlink target\n");
file_handle f = file_open_at(file_handle_nil(), STR8("./data/symlink"), FILE_ACCESS_READ, 0);
if(file_last_error(f))
{
log_error("failed to open ./data/symlink\n");
return(-1);
}
if(check_string(f, STR8("Hello from regular.txt")))
{
log_error("Check string failed\n");
return(-1);
}
file_close(f);
// open symlink file
log_info("open symlink file\n");
f = file_open_at(file_handle_nil(), STR8("./data/symlink"), FILE_ACCESS_READ, FILE_OPEN_SYMLINK);
if(file_last_error(f))
{
log_error("failed to open ./data/symlink\n");
return(-1);
}
file_status status = file_get_status(f);
if(file_last_error(f))
{
log_error("Error while retrieving file status\n");
return(-1);
}
if(status.type != MP_FILE_SYMLINK)
{
log_error("file type doesn't match\n");
return(-1);
}
char buffer[512];
int n = file_read(f, 512, buffer);
if(n || (file_last_error(f) == IO_OK))
{
log_error("file read should fail on symlinks\n");
return(-1);
}
file_close(f);
return(0);
}
int test_args()
{
//NOTE: nil handle
log_info("check open_at with nil handle\n");
file_handle f = file_open_at(file_handle_nil(), STR8("./data/regular.txt"), FILE_ACCESS_READ, 0);
if(file_last_error(f))
{
log_error("file_open_at() with nil handle failed\n");
return(-1);
}
if(check_string(f, STR8("Hello from regular.txt")))
{
log_error("Check string failed\n");
return(-1);
}
file_close(f);
//NOTE: invalid handle
log_info("check open_at with nil handle\n");
file_handle wrongHandle = {.h = 123456789 };
f = file_open_at(wrongHandle, STR8("./data/regular.txt"), FILE_ACCESS_READ, 0);
if(file_last_error(f) != IO_ERR_HANDLE)
{
log_error("file_open_at() with non-nil invalid handle should return IO_ERR_HANDLE\n");
return(-1);
}
file_close(f);
//NOTE: nil/wrong handle and FILE_OPEN_RESTRICT
log_info("check open_at with nil handle and FILE_OPEN_RESTRICT\n");
f = file_open_at(file_handle_nil(), STR8("./data/regular.txt"), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) != IO_ERR_HANDLE)
{
log_error("file_open_at() with nil handle and FILE_OPEN_RESTRICT should return IO_ERR_HANDLE\n");
return(-1);
}
file_close(f);
f = file_open_at(wrongHandle, STR8("./data/regular.txt"), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) != IO_ERR_HANDLE)
{
log_error("file_open_at() with invalid handle and FILE_OPEN_RESTRICT should return IO_ERR_HANDLE\n");
return(-1);
}
file_close(f);
//NOTE: empty path
log_info("check empty path\n");
f = file_open_at(file_handle_nil(), STR8(""), FILE_ACCESS_READ, 0);
if(file_last_error(f) != IO_ERR_ARG)
{
log_error("empty path should return IO_ERR_ARG\n");
return(-1);
}
file_close(f);
return(0);
}
int test_jail()
{
log_info("test jail\n");
@ -197,8 +328,22 @@ int test_jail()
return(-1);
}
// Check escapes
file_handle f = file_open_at(jail, STR8(".."), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
//-----------------------------------------------------------
//NOTE: Check escapes
//-----------------------------------------------------------
log_info("check potential escapes\n");
//NOTE: escape with absolute path
file_handle f = file_open_at(jail, STR8("/tmp"), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) != IO_ERR_NO_ENTRY)
{
log_error("Escaped jail with absolute path /tmp\n");
return(-1);
}
file_close(f);
//NOTE: escape with ..
f = file_open_at(jail, STR8(".."), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) != IO_ERR_WALKOUT)
{
log_error("Escaped jail with relative path ..\n");
@ -206,47 +351,134 @@ int test_jail()
}
file_close(f);
f = file_open_at(jail, STR8(".."), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
//NOTE: escape with dir/../..
f = file_open_at(jail, STR8("dir/../.."), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) != IO_ERR_WALKOUT)
{
log_error("Escaped jail with relative path dir1/../..\n");
log_error("Escaped jail with relative path dir/../..\n");
return(-1);
}
file_close(f);
f = file_open_at(jail, STR8("/escape"), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
//NOTE: escape with symlink to parent
f = file_open_at(jail, STR8("/dir_escape"), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) != IO_ERR_WALKOUT)
{
log_error("Escaped jail with symlink\n");
log_error("Escaped jail with symlink to parent\n");
return(-1);
}
file_close(f);
// Check legitimates open
//NOTE: escape to file with symlink to parent
f = file_open_at(jail, STR8("/dir_escape/regular.txt"), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) != IO_ERR_WALKOUT)
{
log_error("Escaped jail to regular.txt with symlink to parent\n");
return(-1);
}
file_close(f);
//NOTE: escape with symlink to file
f = file_open_at(jail, STR8("/file_escape"), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) != IO_ERR_WALKOUT)
{
log_error("Escaped jail with symlink to file regular.txt\n");
return(-1);
}
file_close(f);
//NOTE: escape with bad root handle
file_handle wrong_handle = {0};
f = file_open_at(wrong_handle, STR8("./data/regular.txt"), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) == IO_OK)
{
log_error("Escaped jail with nil root handle\n");
return(-1);
}
if(file_last_error(f) != IO_ERR_HANDLE)
{
log_error("FILE_OPEN_RESTRICT with invalid root handle should return IO_ERR_HANDLE\n");
return(-1);
}
file_close(f);
//-----------------------------------------------------------
//NOTE: empty path
//-----------------------------------------------------------
log_info("check empty path\n");
f = file_open_at(jail, STR8(""), FILE_ACCESS_READ, 0);
if(file_last_error(f) != IO_ERR_ARG)
{
log_error("empty path should return IO_ERR_ARG\n");
return(-1);
}
file_close(f);
//-----------------------------------------------------------
//NOTE: Check legitimates open
//-----------------------------------------------------------
log_info("check legitimates open\n");
//NOTE: regular file jail/test.txt
f = file_open_at(jail, STR8("/test.txt"), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) != IO_OK)
{
log_error("Can't open jail/test.txt\n");
return(-1);
}
file_close(f);
f = file_open_at(jail, STR8("/dir1/../test.txt"), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) != IO_OK)
if(check_string(f, STR8("Hello from jail/test.txt")))
{
log_error("Can't open jail/dir1/../test.txt\n");
log_error("Check string failed\n");
return(-1);
}
file_close(f);
//NOTE: valid file traversal to jail/test.txt
f = file_open_at(jail, STR8("/dir/../test.txt"), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) != IO_OK)
{
log_error("Can't open jail/dir/../test.txt\n");
return(-1);
}
if(check_string(f, STR8("Hello from jail/test.txt")))
{
log_error("Check string failed\n");
return(-1);
}
file_close(f);
//NOTE: re-open root directory
f = file_open_at(jail, STR8("."), FILE_ACCESS_READ, FILE_OPEN_RESTRICT);
if(file_last_error(f) != IO_OK)
{
log_error("Can't open jail/.\n");
return(-1);
}
{
//NOTE: access regular file test.txt inside reopened root
file_handle f2 = file_open_at(f, STR8("test.txt"), FILE_ACCESS_READ, 0);
if(check_string(f2, STR8("Hello from jail/test.txt")))
{
log_error("Check string failed\n");
return(-1);
}
file_close(f2);
}
file_close(f);
return(0);
}
int test_rights(mem_arena* arena, str8 dirPath)
int test_rights()
{
log_info("test rights\n");
str8 dirPath = STR8("./data");
//--------------------------------------------------------------------------------------
// base dir with no access
//--------------------------------------------------------------------------------------
@ -395,19 +627,16 @@ int main(int argc, char** argv)
mem_arena* arena = mem_scratch();
str8 dataDir = STR8("./data");
str8 path = STR8("./test.txt");
str8 test_string = STR8("Hello, world!");
if(test_write(arena, path, test_string)) { return(-1); }
if(test_read(arena, path, test_string)) { return(-1); }
if(test_stat_size(path, test_string.len)) { return(-1); }
if(test_stat_type(arena, dataDir)) { return(-1); }
if(test_rights(arena, dataDir)) { return(-1); }
if(test_write()) { return(-1); }
if(test_read()) { return(-1); }
if(test_stat_size()) { return(-1); }
if(test_stat_type()) { return(-1); }
if(test_args()) { return(-1); }
if(test_symlinks()) { return(-1); }
if(test_rights()) { return(-1); }
if(test_jail()) { return(-1); }
remove("./test.txt");
remove("./data/write_test.txt");
log_info("OK\n");