blob: 5e5a807519f8ef0e72850f47c2f18b285d636e6e [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include <CUnit/CUnit.h>
#include <guacamole/file.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
/**
* Closes the given file descriptor if it is not an error code from a previous
* call to open() or openat().
*
* @param fd
* The file descriptor to close.
*/
static void close_if_necessary(int fd) {
if (fd != -1)
close(fd);
}
/**
* Returns whether a file with the given filename exists beneath the given
* path.
*
* @param path
* The path to the directory that may contain the file.
*
* @param filename
* The filename to test.
*
* @return
* Non-zero if a file exists with the given filename beneath the given
* path, zero otherwise.
*/
static int exists(const char* path, const char* filename) {
int result = 0;
int dir_fd = open(path, O_RDONLY);
if (dir_fd == -1)
return 0;
struct stat file_info;
result = fstatat(dir_fd, filename, &file_info, 0);
close_if_necessary(dir_fd);
return result != -1;
}
/**
* Removes the file with the given filename beneath the given path. If NULL is
* provided instead of a filename, the final component of the given path is
* removed as a directory.
*
* @param path
* The path to the directory that contains the file to remove.
*
* @param filename
* The filename to remove, or NULL if the containing directory should be
* removed instead.
*
* @return
* Non-zero if the operation succeeded, zero otherwise.
*/
static int remove_file(const char* path, const char* filename) {
if (filename == NULL)
return !rmdir(path);
int dir_fd = open(path, O_RDONLY);
if (dir_fd == -1)
return 0;
int result = unlinkat(dir_fd, filename, 0);
close_if_necessary(dir_fd);
return !result;
}
/**
* Returns whether the file with the given filename beneath the given path has
* the given permissions (mode). Any permission bits that apply to the file but
* which are greater than the least-significant 12 bits are ignored.
*
* @param path
* The path to the directory that contains the file to test.
*
* @param filename
* The filename to test.
*
* @param mode
* The permissions (mode) that the file is expected to have.
*
* @return
* Non-zero if the file does have the given permissions, zero otherwise.
*/
static int has_mode(const char* path, const char* filename, mode_t mode) {
int dir_fd = open(path, O_RDONLY);
if (dir_fd == -1)
return 0;
struct stat file_info = { 0 };
int result = fstatat(dir_fd, filename, &file_info, 0);
close_if_necessary(dir_fd);
return result == 0 && (file_info.st_mode & 07777) == mode;
}
#ifndef __MINGW32__
/**
* Returns whether another process would be able to acquire the lock of the
* given type on the given file. If no such lock can be acquired due to
* conflicts (another process already holds a conflicting lock), non-zero is
* returned.
*
* @param path
* The path to the directory that contains the file to test.
*
* @param filename
* The filename to test.
*
* @param lock_type
* The type of lock to check for, either F_RDLCK or F_WRLCK.
*
* @return
* Non-zero if an existing lock would conflict with the given lock type,
* zero otherwise.
*/
static int lock_conflicts(const char* path, const char* filename, short lock_type) {
int result = 0;
pid_t pid = fork();
CU_ASSERT_NOT_EQUAL_FATAL(pid, -1);
/* In child */
if (pid == 0) {
int dir_fd = open(path, O_RDONLY);
if (dir_fd == -1)
_exit(0);
int fd = openat(dir_fd, filename, O_RDONLY);
if (fd != -1) {
struct flock file_lock = {
.l_type = lock_type,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0
};
if (fcntl(fd, F_GETLK, &file_lock) != -1)
result = (file_lock.l_type != F_UNLCK);
}
close_if_necessary(dir_fd);
close_if_necessary(fd);
_exit(result);
}
return waitpid(pid, &result, 0) != -1 && WIFEXITED(result) && WEXITSTATUS(result);
}
#endif
/**
* Verify general guac_openat() behavior when creating files within
* directories, optionally first creating that directory.
*/
void test_file__openat(void) {
int fd;
char filename[1024];
char temp_dir[64] = "/tmp/guacamole-server-test_file__openat.XXXXXX";
CU_ASSERT_PTR_NOT_NULL_FATAL(mkdtemp(temp_dir));
guac_open_how how = {
.oflags = O_CREAT | O_WRONLY,
.mode = S_IRUSR | S_IWUSR | S_IRGRP
};
/* File should be successfully created where there are no other files */
fd = guac_openat(temp_dir, "foo", &how);
CU_ASSERT_NOT_EQUAL(fd, -1);
CU_ASSERT_TRUE(exists(temp_dir, "foo"));
CU_ASSERT_TRUE(has_mode(temp_dir, "foo", how.mode));
close_if_necessary(fd);
/* File should NOT be successfully created if it already exists and we are
* requesting exclusivity */
how.oflags |= O_EXCL;
fd = guac_openat(temp_dir, "foo", &how);
CU_ASSERT_EQUAL(fd, -1);
close_if_necessary(fd);
how.oflags &= ~O_EXCL;
/* File should be successfully opened if it already exists and we are NOT
* requesting exclusivity */
fd = guac_openat(temp_dir, "foo", &how);
CU_ASSERT_NOT_EQUAL(fd, -1);
close_if_necessary(fd);
/* If unique suffix handling is requested, things should still fail if no
* destination buffer is provided for the updated filename */
how.flags |= GUAC_O_UNIQUE_SUFFIX;
fd = guac_openat(temp_dir, "foo", &how);
CU_ASSERT_EQUAL(fd, -1);
close_if_necessary(fd);
/* If unique suffix handling is requested, a numeric suffix should be added
* if the file already exists */
how.filename = filename;
how.filename_size = sizeof(filename);
fd = guac_openat(temp_dir, "foo", &how);
CU_ASSERT_NOT_EQUAL(fd, -1);
CU_ASSERT_TRUE(exists(temp_dir, "foo.1"));
CU_ASSERT_TRUE(has_mode(temp_dir, "foo.1", how.mode));
close_if_necessary(fd);
if (fd != -1)
CU_ASSERT_STRING_EQUAL(filename, "foo.1");
/* Numeric suffixes should continue to increase as necessary */
fd = guac_openat(temp_dir, "foo", &how);
CU_ASSERT_NOT_EQUAL(fd, -1);
CU_ASSERT_TRUE(exists(temp_dir, "foo.2"));
CU_ASSERT_TRUE(has_mode(temp_dir, "foo.2", how.mode));
close_if_necessary(fd);
if (fd != -1)
CU_ASSERT_STRING_EQUAL(filename, "foo.2");
/* Creation within non-existent directories should fail by default ... */
CU_ASSERT_TRUE(remove_file(temp_dir, "foo"));
CU_ASSERT_TRUE(remove_file(temp_dir, "foo.1"));
CU_ASSERT_TRUE(remove_file(temp_dir, "foo.2"));
CU_ASSERT_TRUE(remove_file(temp_dir, NULL));
fd = guac_openat(temp_dir, "foo", &how);
CU_ASSERT_EQUAL(fd, -1);
close_if_necessary(fd);
/* ... but should succeed if automatic path creation is requested */
how.flags |= GUAC_O_CREATE_PATH;
fd = guac_openat(temp_dir, "foo", &how);
CU_ASSERT_NOT_EQUAL(fd, -1);
CU_ASSERT_TRUE(exists(temp_dir, "foo"));
CU_ASSERT_TRUE(has_mode(temp_dir, "foo", how.mode));
close_if_necessary(fd);
/* The resulting filename should be produced so long as
* GUAC_O_UNIQUE_SUFFIX is specified, even if no change is made to the
* filename */
if (fd != -1)
CU_ASSERT_STRING_EQUAL(filename, "foo");
/* Clean up all remaining temporary files */
CU_ASSERT_TRUE(remove_file(temp_dir, "foo"));
CU_ASSERT_TRUE(remove_file(temp_dir, NULL));
}
/**
* Verify guac_openat() behavior when the filename provided contains path
* components.
*/
void test_file__openat_not_filename(void) {
int fd;
char temp_dir[64] = "/tmp/guacamole-server-test_file__openat_not_filename.XXXXXX";
CU_ASSERT_PTR_NOT_NULL_FATAL(mkdtemp(temp_dir));
guac_open_how how = {
.oflags = O_RDONLY
};
/* Path separators should be permitted only in the path */
fd = guac_openat(temp_dir, "foo/bar", &how);
CU_ASSERT_EQUAL(fd, -1);
close_if_necessary(fd);
fd = guac_openat(temp_dir, "foo\\bar", &how);
CU_ASSERT_EQUAL(fd, -1);
close_if_necessary(fd);
/* References to current and parent directories should not be permitted in
* filename (NOTE: All other occurrences are implicitly covered by the path
* separator check) */
fd = guac_openat(temp_dir, ".", &how);
CU_ASSERT_EQUAL(fd, -1);
close_if_necessary(fd);
fd = guac_openat(temp_dir, "..", &how);
CU_ASSERT_EQUAL(fd, -1);
close_if_necessary(fd);
/* Clean up all remaining temporary files */
CU_ASSERT_TRUE(remove_file(temp_dir, NULL));
}
/**
* Verify guac_openat() behavior when locking is requested vs. not requested.
*/
void test_file__openat_locked(void) {
#ifdef __MINGW32__
/* Skipped under Windows platforms (see documentation for GUAC_O_LOCKED) */
#else
int fd;
char temp_dir[64] = "/tmp/guacamole-server-test_file__openat_locked.XXXXXX";
CU_ASSERT_PTR_NOT_NULL_FATAL(mkdtemp(temp_dir));
guac_open_how how = {
.oflags = O_CREAT | O_WRONLY,
.mode = S_IRUSR | S_IWUSR | S_IRGRP
};
/* We should not have any locks unless requested */
fd = guac_openat(temp_dir, "foo", &how);
CU_ASSERT_NOT_EQUAL(fd, -1);
CU_ASSERT_FALSE(lock_conflicts(temp_dir, "foo", F_WRLCK));
CU_ASSERT_FALSE(lock_conflicts(temp_dir, "foo", F_RDLCK));
close_if_necessary(fd);
/* We should have a write lock on the file if opened for writing while
* GUAC_O_LOCKED is set */
how.flags = GUAC_O_LOCKED;
fd = guac_openat(temp_dir, "foo", &how);
CU_ASSERT_NOT_EQUAL(fd, -1);
CU_ASSERT_TRUE(lock_conflicts(temp_dir, "foo", F_WRLCK));
CU_ASSERT_TRUE(lock_conflicts(temp_dir, "foo", F_RDLCK));
close_if_necessary(fd);
/* We should have a read lock on the file if opened for reading while
* GUAC_O_LOCKED is set */
how.oflags = O_RDONLY;
how.mode = 0;
fd = guac_openat(temp_dir, "foo", &how);
CU_ASSERT_NOT_EQUAL(fd, -1);
CU_ASSERT_TRUE(lock_conflicts(temp_dir, "foo", F_WRLCK));
CU_ASSERT_FALSE(lock_conflicts(temp_dir, "foo", F_RDLCK));
close_if_necessary(fd);
/* Clean up all remaining temporary files */
CU_ASSERT_TRUE(remove_file(temp_dir, "foo"));
CU_ASSERT_TRUE(remove_file(temp_dir, NULL));
#endif
}