#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { std::atomic ShuttingDown{}; void SignalShutdown(); namespace EPollWatcher { static int epoll_fd{}; static std::thread EPollThread{}; static std::atomic EPollThreadTID{}; static rlimit MaxFDs{}; constexpr size_t static MAX_FD_DISTANCE = 32; std::atomic NumPipesWatched{}; std::atomic NumFilesOpened{}; std::atomic EPollWatcherShutdown {false}; std::chrono::time_point TimeWhileZeroFDs{}; // Timeout is ten seconds constexpr std::chrono::duration TimeoutPeriod = std::chrono::seconds(10); uint64_t NumPipesRemaining() { return NumPipesWatched.load(); } size_t GetNumFilesOpen() { // Walk /proc/self/fd/ to see how many open files we currently have const std::filesystem::path self{"/proc/self/fd/"}; return std::distance(std::filesystem::directory_iterator{self}, std::filesystem::directory_iterator{}); } void GetMaxFDs() { // Get our kernel limit for the number of open files if (getrlimit(RLIMIT_NOFILE, &MaxFDs) != 0) { fprintf(stderr, "[FEXMountDaemon] getrlimit(RLIMIT_NOFILE) returned error %d %s\n", errno, strerror(errno)); } // Walk /proc/self/fd/ to see how many open files we currently have NumFilesOpened = GetNumFilesOpen(); } void RaiseFDLimit() { if (MaxFDs.rlim_cur == MaxFDs.rlim_max) { fprintf(stderr, "[FEXMountDaemon] Our open FD limit is already set to max and we are wanting to increase it\n"); fprintf(stderr, "[FEXMountDaemon] FEXMountDaemon will now no longer be able to track new instances of FEX\n"); fprintf(stderr, "[FEXMountDaemon] Current limit is %zd(hard %zd) FDs and we are at %zd\n", MaxFDs.rlim_cur, MaxFDs.rlim_max, GetNumFilesOpen()); fprintf(stderr, "[FEXMountDaemon] Ask your administrator to raise your kernel's hard limit on open FDs\n"); return; } rlimit NewLimit = MaxFDs; // Just multiply by two NewLimit.rlim_cur <<= 1; // Now limit to the hard max NewLimit.rlim_cur = std::min(NewLimit.rlim_cur, NewLimit.rlim_max); if (setrlimit(RLIMIT_NOFILE, &NewLimit) != 0) { fprintf(stderr, "[FEXMountDaemon] Couldn't raise FD limit to %zd even though our hard limit is %zd\n", NewLimit.rlim_cur, NewLimit.rlim_max); } else { // Set the new limit MaxFDs = NewLimit; } } void AddPipeToWatch(int pipe) { struct epoll_event evt{}; evt.events = EPOLLERR; // This event will return when the read end of a pipe is closed evt.data.fd = pipe; // Just return the pipe in the user data int Result = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, pipe, &evt); if (Result == -1) { fprintf(stderr, "[FEXMountDaemon] epoll_ctl returned error %d %s\n", errno, strerror(errno)); } else { ++NumPipesWatched; if ((NumPipesWatched + NumFilesOpened) >= (MaxFDs.rlim_cur - MAX_FD_DISTANCE)) { // We are close to the maximum FD distance // Try to raise the limit RaiseFDLimit(); } } } void RemovePipeToWatch(int pipe) { int Result = epoll_ctl(epoll_fd, EPOLL_CTL_DEL, pipe, nullptr); // Make sure to close the pipe to not leak the FD close(pipe); if (Result == -1) { fprintf(stderr, "[FEXMountDaemon] epoll_ctl returned error %d %s\n", errno, strerror(errno)); } else { --NumPipesWatched; } // If the number of pipes we are watching drops to zero then start a timer so we can exit if (NumPipesRemaining() == 0) { TimeWhileZeroFDs = std::chrono::system_clock::now(); } } void EPollWatch() { pthread_setname_np(pthread_self(), "EPollWatcher"); EPollThreadTID = FHU::Syscalls::gettid(); constexpr size_t MAX_EVENTS = 16; struct epoll_event Events[MAX_EVENTS]{}; // Spin while we are not shutting down // Also spin while we have pipes to watch while (!EPollWatcherShutdown.load()) { // Loop every ten seconds // epoll_pwait2 only available since kernel 5.11... int Result = epoll_pwait(epoll_fd, Events, MAX_EVENTS, 10 * 1000, nullptr); if (Result == -1) { // EINTR is common here } else { for (size_t i = 0; i < Result; ++i) { auto &Event = Events[i]; if (Event.events & EPOLLERR) { // This pipe's read end has closed // No need to watch it anymore RemovePipeToWatch(Event.data.fd); } } // If we are at zero pipes then check our timer if we are past the timeout period to exit if (NumPipesRemaining() == 0) { auto Now = std::chrono::system_clock::now(); auto Dur = Now - TimeWhileZeroFDs; if (Dur >= TimeoutPeriod) { // We need to shutdown now ::SignalShutdown(); } } } } // Close the poll_fd close(epoll_fd); } void SetupEPoll() { GetMaxFDs(); epoll_fd = epoll_create1(EPOLL_CLOEXEC); EPollThread = std::thread{EPollWatcher::EPollWatch}; } void SignalShutdown() { EPollWatcherShutdown = true; // Send a signal to wake up epoll immediately FHU::Syscalls::tgkill(::getpid(), EPollThreadTID, SIGUSR1); } void ShutdownEPoll() { SignalShutdown(); EPollThread.join(); } } namespace SocketWatcher { static int socket_fd{}; static std::thread SocketThread{}; static std::atomic SocketThreadTID{}; const std::string *SocketWatcherPath{}; std::atomic SocketShutdown {false}; void SocketFunction() { pthread_setname_np(pthread_self(), "SocketWatcher"); SocketThreadTID = FHU::Syscalls::gettid(); listen(socket_fd, 16); while (!SocketShutdown.load()) { // Listen for connections // Wait for new clients coming in struct pollfd pfd{}; pfd.fd = socket_fd; pfd.events = POLLIN; // Wait for ten seconds struct timespec ts{}; ts.tv_sec = 10; int Result = ppoll(&pfd, 1, &ts, nullptr); if (Result > 0) { struct sockaddr_in from{}; socklen_t addrlen = sizeof(from); int new_client = accept(socket_fd, reinterpret_cast(&from), &addrlen); // Wait for data coming in struct pollfd pfd{}; pfd.fd = new_client; pfd.events = POLLIN; // Wait for ten seconds struct timespec ts{}; ts.tv_sec = 10; int Result = ppoll(&pfd, 1, &ts, nullptr); if (Result == -1) { // EINTR is common here } else if (Result > 0) { // We got data to grab struct msghdr msg{}; struct iovec iov{}; char iov_data{}; // Setup the ancillary buffer. This is where we will be getting pipe FDs // We only need 4 bytes for the FD constexpr size_t CMSG_SIZE = CMSG_SPACE(sizeof(int)); union AncillaryBuffer { struct cmsghdr Header; uint8_t Buffer[CMSG_SIZE]; }; AncillaryBuffer AncBuf{}; // Set up message header msg.msg_name = nullptr; msg.msg_namelen = 0; msg.msg_iov = &iov; msg.msg_iovlen = 1; // Setup iov. We won't be receiving any real data here iov.iov_base = &iov_data; iov.iov_len = sizeof(iov_data); // Now link to our ancilllary buffer msg.msg_control = AncBuf.Buffer; msg.msg_controllen = CMSG_SIZE; ssize_t DataResult = recvmsg(new_client, &msg, 0); if (DataResult == -1) { // Sometimes get a spurious read? } else { // Now that we have the data, we can extract the FD from the ancillary buffer struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); // Do some error checking if (cmsg == nullptr || cmsg->cmsg_len != CMSG_LEN(sizeof(int)) || cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) { fprintf(stderr, "[FEXMountDaemon:SocketWatcher] cmsg data was incorrect\n"); } else { // Now that we know the cmsg is sane, read the FD int NewFD{}; memcpy(&NewFD, CMSG_DATA(cmsg), sizeof(NewFD)); // Add to to the epoll watcher EPollWatcher::AddPipeToWatch(NewFD); // If we received an event while in the process of shutting down then we have raced // Send back an ack message uint8_t c = 0; send(new_client, &c, sizeof(c), 0); } } } // Now that we have received data on this socket we don't need to keep it around close(new_client); } else { // EINTR common here } } } bool CreateServerSocket(std::string &SocketPath) { // Unlink the socket file if it exists // We are being asked to create a daemon, not error check // We don't care if this failed or not remove(SocketPath.c_str()); // Create the initial unix socket socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (socket_fd == -1) { fprintf(stderr, "[FEXMountDaemon:SocketWatcher] Couldn't create AF_UNIX socket: %d %s\n", errno, strerror(errno)); return false; } struct sockaddr_un addr{}; addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SocketPath.data(), sizeof(addr.sun_path)); // Bind the socket to the path int Result = bind(socket_fd, reinterpret_cast(&addr), sizeof(addr)); if (Result == -1) { fprintf(stderr, "[FEXMountDaemon:SocketWatcher] Couldn't bind AF_UNIX socket '%s': %d %s\n", addr.sun_path, errno, strerror(errno)); return false; } return true; } bool SetupSocketWatcher(std::string &SocketPath) { SocketWatcherPath = &SocketPath; if (!CreateServerSocket(SocketPath)) { return false; } SocketThread = std::thread{SocketWatcher::SocketFunction}; return true; } void SignalShutdown() { SocketShutdown = true; // Send a signal to wake up ppoll immediately FHU::Syscalls::tgkill(::getpid(), SocketThreadTID, SIGUSR1); } void ShutdownSocketWatcher() { SignalShutdown(); SocketThread.join(); // Close the socket and delete the socket file close(socket_fd); remove(SocketWatcherPath->c_str()); } } namespace INotifyWatcher { static int lock_fd {-1}; static std::condition_variable WaitCV{}; static std::mutex WaitMutex{}; static std::atomic INotifyShutdown {false}; const std::string *INotifyLockPath{}; enum LockFailure { LOCK_FAIL_FATAL, LOCK_FAIL_EXISTS, LOCK_FAIL_CREATION_RACE, LOCK_FAIL_CREATED, }; constexpr int USER_PERMS = S_IRWXU | S_IRWXG | S_IRWXO; void RemoveLock(); LockFailure CreateINotifyLock(const std::string &LockPath, const char *MountPath) { INotifyLockPath = &LockPath; lock_fd = open(LockPath.c_str(), O_RDONLY, USER_PERMS); if (lock_fd != -1) { // LockFD already existed! // This will have now refcounted the existing daemon! close(lock_fd); return LOCK_FAIL_EXISTS; } lock_fd = open(LockPath.c_str(), O_CREAT | O_RDWR, USER_PERMS); if (lock_fd == -1) { // Couldn't open lock file for some reason // Likely read only file system return LOCK_FAIL_FATAL; } LockFailure Failure = LOCK_FAIL_FATAL; // Set up the write lock to ensure this doesn't race { // Attempt to open a write lease on the lock file // First thing, mask the signal from the least interface // By default it is SIGIO { sigset_t set; sigemptyset(&set); sigaddset(&set, SIGIO); if (sigprocmask(SIG_BLOCK, &set, nullptr) == -1) { goto err; } } // Set the file's lease signal // Even if we set it to default, this is necessary { int Res = fcntl(lock_fd, F_SETSIG, SIGIO); if (Res == -1) { // Shouldn't fail goto err; } } // Now attempt to get a write lock on this file { int Res = fcntl(lock_fd, F_SETLEASE, F_WRLCK); if (Res == -1) { // Couldn't get a write lock // This means another FEXMountDaemon is in the process of setting up a rootfs // Early exit, this will be mounted in another process Failure = LOCK_FAIL_CREATION_RACE; goto err; } } // Now that we have a lock on the file. // Write where we are going to be mounting // Nothing else can currently open the file for reads yet to see this write(lock_fd, MountPath, strlen(MountPath)); } return LOCK_FAIL_CREATED; err: if (lock_fd != -1) { close (lock_fd); } return Failure; } void WatchLock() { pthread_setname_np(pthread_self(), "INotifyWatcher"); // Let go of the file lease file to allow FEX to continue fcntl(lock_fd, F_SETLEASE, F_UNLCK); // Wait in a mutex to shutdown std::unique_lock lk (WaitMutex); WaitCV.wait(lk, [] { return INotifyShutdown.load(); }); // Remove the locks immediately after shutdown INotifyWatcher::RemoveLock(); } void RemoveLock() { if (lock_fd != -1) { // Grab the file lock so FEX can't open it int Res = fcntl(lock_fd, F_SETLEASE, F_WRLCK); if (Res == -1) { fprintf(stderr, "Couldn't lock lock file: %d %s\n", errno, strerror(errno)); } // remove the contents in case any client is in the process of opening this ftruncate(lock_fd, 0); // Remove the lock file itself unlink(INotifyLockPath->c_str()); // Clear the lease on it since we are shutting down fcntl(lock_fd, F_SETLEASE, F_UNLCK); // Close the lock fd close(lock_fd); lock_fd = -1; } } void SignalShutdown() { INotifyShutdown = true; WaitCV.notify_all(); } } void ActionHandler(int sig, siginfo_t *info, void *context) { if (sig == SIGCHLD) { if (EPollWatcher::NumPipesRemaining() != 0) { // Check the pipes we are watching to see if any exist // If our child process shutdown while our parent is still running // Then the parent loses its rootfs and problems occur fprintf(stderr, "FEXMountDaemon child process from squashfuse has closed while FEX instances still exist\n"); fprintf(stderr, "Expect errors!\n"); } } else if (sig == SIGUSR1) { // Ignore } else { // Signal sent directly to process // Force a shutdown fprintf(stderr, "We are being told to shutdown with SIGTERM\n"); fprintf(stderr, "Watch out! You might get dangling mount points!\n"); ::SignalShutdown(); } } void SignalShutdown() { ::ShuttingDown = true; // We need to very carefully deconstruct our state to ensure we don't have a race condition // First thing to do is to shutdown the lock thread // Race here where the application opens the lock file, reads the mount directory while the file is getting deleted // Nothing we can really do on our side INotifyWatcher::SignalShutdown(); // Now that we have shutdown the lock file. No more NEW clients of FEX will find this daemon // Next race is that any clients that managed to read the lock file before it was erased can find the socket // for this rootfs daemon // Depending on when the socket closes either the client will fail to send an FD to us OR // we receive the FD and start watching it // // On failure to send FD, the client needs to retry FEXMountDaemon mounting SocketWatcher::SignalShutdown(); // Time to handle the tail end of the last race condition // In the case of FEX sending an FD while SocketWatcher is shutting down // // Shutdown() // -> INotifyWatcher::Shutdown() // -> SocketWatcher:: Receives FD // -> EPollWatcher::SignalShutdown // -> EPollWatcher::EPollWatch keeps running until all FDs leave EPollWatcher::SignalShutdown(); } } int main(int argc, char **argv, char **envp) { if (argc < 4) { fprintf(stderr, "usage: %s \n", argv[0]); return -1; } pid_t pid = fork(); if (pid != 0) { // Parent is leaving to force this process to deparent itself // This lets this process become the child of whatever the reaper parent is // Do this up front so a FEXInterptreter instance doesn't erroneously pick up a SIGCHLD return 0; } const char *SquashFSPath = argv[1]; const char *MountPath = argv[2]; int pipe_wr = std::atoi(argv[3]); // Setup our signal handlers now so we can capture some events struct sigaction act{}; act.sa_sigaction = ActionHandler; act.sa_flags = SA_SIGINFO; // SIGCHLD if squashfuse exits early sigaction(SIGCHLD, &act, nullptr); // SIGTERM if something is trying to terminate us sigaction(SIGTERM, &act, nullptr); // SIGUSR1 just to interrupt syscalls sigaction(SIGUSR1, &act, nullptr); // Ignore SIGPIPE, we will be checking for pipe closure which could send this signal signal(SIGPIPE, SIG_IGN); // Start the epoll watcher EPollWatcher::SetupEPoll(); // Switch this process over to a new session id // Probably not required but allows this to become the process group leader of its session ::setsid(); // Create local FDs so our internal forks can communicate int localfds[2]; pipe2(localfds, 0); ::prctl(PR_SET_CHILD_SUBREAPER, 1); struct utsname uts{}; uname (&uts); std::string LockPath = "/tmp/.FEX-"; LockPath += std::filesystem::path(SquashFSPath).filename(); LockPath += ".lock."; LockPath += uts.nodename; std::string SocketPath = MountPath; SocketPath += ".socket"; if (!SocketWatcher::SetupSocketWatcher(SocketPath)) { EPollWatcher::ShutdownEPoll(); return -1; } // Use lock files to ensure we aren't racing to mount multiple FSes auto Failure = INotifyWatcher::CreateINotifyLock(LockPath, MountPath); if (Failure == INotifyWatcher::LOCK_FAIL_FATAL) { EPollWatcher::ShutdownEPoll(); // Shutdown the signal watcher SocketWatcher::ShutdownSocketWatcher(); return -1; } if (Failure == INotifyWatcher::LOCK_FAIL_EXISTS || Failure == INotifyWatcher::LOCK_FAIL_CREATION_RACE) { EPollWatcher::ShutdownEPoll(); // Shutdown the signal watcher SocketWatcher::ShutdownSocketWatcher(); // If the lock already exists // Then we don't need to spin up the mounts at all // Cleanly exit early and let FEX know it can continue uint64_t c = 0; write(pipe_wr, &c, sizeof(c)); return 0; } pid = fork(); if (pid == 0) { // Child close(localfds[0]); // Close read side const char *argv[4]; argv[0] = "/usr/bin/squashfuse"; argv[1] = SquashFSPath; argv[2] = MountPath; argv[3] = nullptr; // Try and execute squashfuse to mount our rootfs if (execve(argv[0], (char * const*)argv, envp) == -1) { // Let the parent know that we couldn't execute for some reason uint64_t error{1}; write(localfds[1], &error, sizeof(error)); // Give a hopefully helpful error message for users fprintf(stderr, "'%s' Couldn't execute for some reason: %d %s\n", argv[0], errno, strerror(errno)); fprintf(stderr, "To mount squashfs rootfs files you need squashfuse installed\n"); fprintf(stderr, "Check your FUSE setup.\n"); // End the child exit(1); } } else { // Parent close(localfds[1]); // Close the write side // Wait for the child to exit // This will happen with execve of squashmount or exit on failure waitpid(pid, nullptr, 0); // Check the child pipe for messages pollfd PollFD; PollFD.fd = localfds[0]; PollFD.events = POLLIN; int Result = poll(&PollFD, 1, 0); if (Result == 1 && PollFD.revents & POLLIN) { // Child couldn't execve for whatever reason // Remove the mount path and leave Just in case it was created rmdir(MountPath); // Tell FEX that we encountered an issue uint64_t c = 1; write(pipe_wr, &c, sizeof(c)); return 1; } // Close the pipe now close(localfds[0]); // Tell FEX that it can continue booting // FEX will stall on opening the lock file until we let go of the lease uint64_t c = 0; write(pipe_wr, &c, sizeof(c)); // Watch our lock file now for users // Blocks here until no more users INotifyWatcher::WatchLock(); // Shutdown the socket watcher fully if it still hasn't SocketWatcher::ShutdownSocketWatcher(); // Shutdown and spin on any epoll FDs remaining EPollWatcher::ShutdownEPoll(); // Handle final mount removal // fusermount for unmounting the mountpoint, then the squashfuse will exit automatically pid = fork(); if (pid == 0) { const char *argv[5]; argv[0] = "/bin/fusermount"; argv[1] = "-u"; argv[2] = "-q"; argv[3] = MountPath; argv[4] = nullptr; if (execve(argv[0], (char * const*)argv, envp) == -1) { // Try another location argv[0] = "/usr/bin/fusermount"; if (execve(argv[0], (char * const*)argv, envp) == -1) { fprintf(stderr, "fusermount failed to execute. You may have an mount living at '%s' to clean up now\n", SquashFSPath); fprintf(stderr, "Try `%s %s %s %s`\n", argv[0], argv[1], argv[2], argv[3]); exit(1); } } } else { // Wait for fusermount to leave waitpid(pid, nullptr, 0); // Remove the mount path and leave rmdir(MountPath); } } return 0; }