From 00b4cbafd01d8390945112af60e38bd5240676f1 Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Wed, 24 Jun 2026 00:15:15 +0200 Subject: [PATCH] Merge the CLI and Daemon portable interface --- Makefile.am | 27 +- cmdline/app.h | 55 +- cmdline/bw.c | 2 +- cmdline/check.c | 2 +- cmdline/device.c | 2 +- cmdline/dry.c | 2 +- cmdline/dup.c | 2 +- cmdline/elem.c | 2 +- cmdline/handle.c | 2 +- cmdline/import.c | 2 +- cmdline/io.c | 2 +- cmdline/list.c | 2 +- cmdline/locate.c | 2 +- cmdline/main.c | 2 +- cmdline/mingwapp.c | 608 ++++++++++- cmdline/mkstream.c | 2 +- cmdline/mktest.c | 2 +- cmdline/os.h | 160 --- cmdline/parity.c | 2 +- cmdline/pool.c | 2 +- cmdline/rehash.c | 2 +- cmdline/scan.c | 2 +- cmdline/scrub.c | 2 +- cmdline/search.c | 2 +- cmdline/selftest.c | 2 +- cmdline/snapraid.c | 13 +- cmdline/speed.c | 2 +- cmdline/state.c | 6 +- cmdline/status.c | 2 +- cmdline/stream.c | 2 +- cmdline/support.c | 121 ++- cmdline/support.h | 3 +- cmdline/sync.c | 2 +- cmdline/thermal.c | 4 +- cmdline/touch.c | 2 +- cmdline/unix.c | 832 --------------- cmdline/unixapp.c | 125 +-- cmdline/util.c | 2 +- configure.ac | 4 +- {cmdline => os}/mingw.c | 2016 ++++++++++++++++++++++-------------- {cmdline => os}/mingw.h | 135 ++- os/os.h | 319 ++++++ {cmdline => os}/portable.h | 56 +- os/unix.c | 1607 ++++++++++++++++++++++++++++ {cmdline => os}/unix.h | 35 +- tommyds/tommy.c | 2 +- 46 files changed, 4132 insertions(+), 2050 deletions(-) delete mode 100644 cmdline/os.h delete mode 100644 cmdline/unix.c rename {cmdline => os}/mingw.c (63%) rename {cmdline => os}/mingw.h (74%) create mode 100644 os/os.h rename {cmdline => os}/portable.h (89%) create mode 100644 os/unix.c rename {cmdline => os}/unix.h (82%) diff --git a/Makefile.am b/Makefile.am index d17b4d3d..ebe42541 100644 --- a/Makefile.am +++ b/Makefile.am @@ -11,7 +11,9 @@ endif # Windows if HOST_WINDOWS -snapraid_LDADD = resource_res.o +snapraid_LDADD = resource_res.o -ladvapi32 -luserenv +mktest_LDADD = -ladvapi32 -luserenv +mkstream_LDADD = -ladvapi32 -luserenv resource_res.o: cmdline/resource.rc cmdline/resource.manifest $(WINDRES) --include-dir=. --include-dir=$(srcdir) $< -o $@ @@ -64,9 +66,9 @@ snapraid_SOURCES = \ cmdline/import.c \ cmdline/search.c \ cmdline/thermal.c \ - cmdline/mingw.c \ + os/mingw.c \ cmdline/mingwapp.c \ - cmdline/unix.c \ + os/unix.c \ cmdline/unixapp.c noinst_HEADERS = \ @@ -92,7 +94,6 @@ noinst_HEADERS = \ tommyds/tommyhashdyn.c \ tommyds/tommylist.c \ tommyds/tommytree.c \ - cmdline/portable.h \ cmdline/snapraid.h \ cmdline/io.h \ cmdline/bw.h \ @@ -115,24 +116,26 @@ noinst_HEADERS = \ cmdline/import.h \ cmdline/search.h \ cmdline/thermal.h \ - cmdline/mingw.h \ - cmdline/unix.h \ - cmdline/os.h \ - cmdline/app.h + cmdline/app.h \ + os/os.h \ + os/mingw.h \ + os/unix.h \ + os/os.h \ + os/portable.h mktest_SOURCES = \ cmdline/mktest.c \ - cmdline/mingw.c \ + os/mingw.c \ cmdline/mingwapp.c \ - cmdline/unix.c \ + os/unix.c \ cmdline/unixapp.c \ cmdline/support.c mkstream_SOURCES = \ cmdline/mkstream.c \ - cmdline/mingw.c \ + os/mingw.c \ cmdline/mingwapp.c \ - cmdline/unix.c \ + os/unix.c \ cmdline/unixapp.c \ cmdline/support.c \ cmdline/util.c \ diff --git a/cmdline/app.h b/cmdline/app.h index 20675671..06d35753 100644 --- a/cmdline/app.h +++ b/cmdline/app.h @@ -4,11 +4,22 @@ #ifndef __APP_H #define __APP_H +#include "os/os.h" + /** * Include list support to have tommy_node. */ #include "tommyds/tommylist.h" +#define EINTERNAL EFAULT /**< Internal assertion failed. */ +#define EDATA EIO /**< Silent data corruption. */ +#define ESOFT EINVAL /**< Software error, like permission denied. */ +#define EUSER EINVAL /**< Invalid value specified by the user. */ +#define EEXTERNAL EINVAL /**< Invalid external interface behaviour. */ +#define ECONTENT EINVAL /**< Invalid content file. */ +#define EENVIRONMENT EINVAL /**< Invalid physical environment, like temperature too high. */ +#define ESYSLOG EINVAL /**< Syslog error. */ + /** * Basic block position type. */ @@ -34,34 +45,6 @@ static inline int hardlink(const char* a, const char* b) */ int devuuid(uint64_t device_id, const char* device_path, char* uuid, size_t size); -/** - * Physical offset not yet read. - */ -#define FILEPHY_UNREAD_OFFSET 0 - -/** - * Special value returned when the file-system doesn't report any offset for unknown reason. - */ -#define FILEPHY_UNREPORTED_OFFSET 1 - -/** - * Special value returned when the file doesn't have a real offset. - * For example, because it's stored in the NTFS MFT. - */ -#define FILEPHY_WITHOUT_OFFSET 2 - -/** - * Value indicating real offsets. All offsets greater or equal at this one are real. - */ -#define FILEPHY_REAL_OFFSET 3 - -/** - * Get the physical address of the specified file. - * This is expected to be just a hint and not necessarily correct or unique. - * Return 0 on success. - */ -int filephy(const char* path, uint64_t size, uint64_t* physical); - /** * Check if the underline file-system support persistent inodes. * Return -1 on error, 0 on success. @@ -393,17 +376,6 @@ int ambient_temperature(void); */ #define WINDOWS_SPACEHOLDER_SIZE (256 * 1024 * 1024) -/** - * Generic errors - */ -#define EINTERNAL EFAULT /**< Internal assertion failed. */ -#define EDATA EIO /**< Silent data corruption. */ -#define ESOFT EINVAL /**< Software error, like permission denied. */ -#define EUSER EINVAL /**< Invalid value specified by the user. */ -#define EEXTERNAL EINVAL /**< Invalid external interface behaviour. */ -#define ECONTENT EINVAL /**< Invalid content file. */ -#define EENVIRONMENT EINVAL /**< Invalid physical environment, like temperature too high. */ - /****************************************************************************/ /* app */ @@ -427,10 +399,5 @@ void app_default_conf(char* dst, size_t dst_size, const char* argv0); */ void app_signal_handler(int signum); -/** - * Global variable to identify if Ctrl+C is pressed. - */ -int app_global_interrupt(void); - #endif diff --git a/cmdline/bw.c b/cmdline/bw.c index 92852c7c..46f3bb5a 100644 --- a/cmdline/bw.c +++ b/cmdline/bw.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2016 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "bw.h" diff --git a/cmdline/check.c b/cmdline/check.c index 2adeae65..f13639ec 100644 --- a/cmdline/check.c +++ b/cmdline/check.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "util.h" diff --git a/cmdline/device.c b/cmdline/device.c index 6473881d..b7194139 100644 --- a/cmdline/device.c +++ b/cmdline/device.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2015 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "state.h" diff --git a/cmdline/dry.c b/cmdline/dry.c index c83633ef..0063887a 100644 --- a/cmdline/dry.c +++ b/cmdline/dry.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "util.h" #include "elem.h" diff --git a/cmdline/dup.c b/cmdline/dup.c index ab734b5b..5c88cb11 100644 --- a/cmdline/dup.c +++ b/cmdline/dup.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "util.h" diff --git a/cmdline/elem.c b/cmdline/elem.c index 410e78ef..0255fdda 100644 --- a/cmdline/elem.c +++ b/cmdline/elem.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "elem.h" #include "support.h" diff --git a/cmdline/handle.c b/cmdline/handle.c index 500950e4..9a979d0e 100644 --- a/cmdline/handle.c +++ b/cmdline/handle.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "elem.h" #include "support.h" diff --git a/cmdline/import.c b/cmdline/import.c index 6f8a9b8c..931f6b02 100644 --- a/cmdline/import.c +++ b/cmdline/import.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2013 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "import.h" diff --git a/cmdline/io.c b/cmdline/io.c index 44973c9b..ad5f6c6f 100644 --- a/cmdline/io.c +++ b/cmdline/io.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2016 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "io.h" diff --git a/cmdline/list.c b/cmdline/list.c index fc41435b..5f274356 100644 --- a/cmdline/list.c +++ b/cmdline/list.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2013 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "util.h" #include "elem.h" diff --git a/cmdline/locate.c b/cmdline/locate.c index eab80b22..11839485 100644 --- a/cmdline/locate.c +++ b/cmdline/locate.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2026 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "elem.h" #include "parity.h" diff --git a/cmdline/main.c b/cmdline/main.c index 3da1f822..f1b9959d 100644 --- a/cmdline/main.c +++ b/cmdline/main.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2025 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "snapraid.h" diff --git a/cmdline/mingwapp.c b/cmdline/mingwapp.c index 395606a3..20d8d6fd 100644 --- a/cmdline/mingwapp.c +++ b/cmdline/mingwapp.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #ifdef __MINGW32__ /* Only for MingW */ @@ -22,7 +22,7 @@ int exit_sync_needed = 2; volatile int global_interrupt = 0; -int app_global_interrupt(void) +int os_signal_interrupt(void) { return global_interrupt; } @@ -33,6 +33,598 @@ void app_signal_handler(int signum) global_interrupt = signum; } +/****************************************************************************/ +/* fs */ + +int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_hardlinks, uint64_t* total_space, uint64_t* free_space, char* fstype, size_t fstype_size, char* fslabel, size_t fslabel_size) +{ + wchar_t conv_buf[CONV_MAX]; + + /* all FAT/exFAT/NTFS when managed from Windows have persistent inodes */ + if (has_persistent_inode) + *has_persistent_inode = 1; + + /* NTFS doesn't synchronize hardlinks metadata */ + if (has_syncronized_hardlinks) + *has_syncronized_hardlinks = 0; + + if (free_space || total_space) { + ULARGE_INTEGER total_bytes; + ULARGE_INTEGER total_free_bytes; + DWORD attr; + char dir[PATH_MAX]; + + if (strlen(path) + 1 > sizeof(dir)) { + windows_errno(ERROR_BUFFER_OVERFLOW); + return -1; + } + + strcpy(dir, path); + + /* get the file attributes */ + attr = GetFileAttributesW(convert(conv_buf, dir)); + if (attr == INVALID_FILE_ATTRIBUTES) { + DWORD error = GetLastError(); + + if (error != ERROR_FILE_NOT_FOUND) { + windows_errno(error); + return -1; + } + + /* + * If it doesn't exist, we assume a file + * and we check for the containing dir + */ + attr = 0; + } + + /* if it's not a directory, truncate the file name */ + if ((attr & FILE_ATTRIBUTE_DIRECTORY) == 0) { + char* slash = strrchr(dir, '/'); + + /** + * Cut the file name, but leave the last slash. + * + * This is done because a MSDN comment about using of UNC paths. + * + * MSDN 'GetDiskFreeSpaceEx function' + * http://msdn.microsoft.com/en-us/library/windows/desktop/aa364937%28v=vs.85%29.aspx + * If this parameter is a UNC name, it must include a trailing backslash, + * for example, "\\MyServer\MyShare\". + */ + if (slash) + slash[1] = 0; + } + + /* + * Get the free space of the directory + * note that it must be a directory + */ + if (!GetDiskFreeSpaceExW(convert(conv_buf, dir), 0, &total_bytes, &total_free_bytes)) { + windows_errno(GetLastError()); + return -1; + } + + if (total_space) + *total_space = total_bytes.QuadPart; + if (free_space) + *free_space = total_free_bytes.QuadPart; + } + + if (fstype && fslabel) { + wchar_t volume_root[PATH_MAX]; + wchar_t fs_name[PATH_MAX]; + wchar_t vol_name[PATH_MAX]; + + fstype[0] = 0; + fslabel[0] = 0; + if (GetVolumePathNameW(convert(conv_buf, path), volume_root, PATH_MAX)) { + if (GetVolumeInformationW(volume_root, vol_name, PATH_MAX, 0, 0, 0, fs_name, PATH_MAX)) { + char u8[CONV_MAX]; + size_t len; + char* ret; + + ret = u16tou8_mayfail(u8, sizeof(u8), fs_name, wcslen(fs_name), &len); + if (ret != 0 && len + 1 <= fstype_size) { + memcpy(fstype, u8, len); + fstype[len] = 0; + } + + ret = u16tou8_mayfail(u8, sizeof(u8), vol_name, wcslen(vol_name), &len); + if (ret != 0 && len + 1 <= fslabel_size) { + memcpy(fslabel, u8, len); + fslabel[len] = 0; + } + } + } + } + + return 0; +} + +/****************************************************************************/ +/* snapshot */ + +size_t windows_direct_size(void) +{ + SYSTEM_INFO si; + + GetSystemInfo(&si); + + /* + * MSDN 'File Buffering' + * https://msdn.microsoft.com/en-us/library/windows/desktop/cc644950%28v=vs.85%29.aspx + * + * "Therefore, in most situations, page-aligned memory will also be sector-aligned," + * "because the case where the sector size is larger than the page size is rare." + */ + return si.dwPageSize; +} + +#define WINDOWS_NTFS_MAGIC 0x5346544E /* 'N','T','F','S' */ + +#define PS_CMD_MAX 4096 + +#define SNAPSHOT_GUID ".guid" + +/* + * PowerShell helper + * + * Runs a PowerShell one-liner via _popen() and captures the first line + * of stdout into `out` as UTF-8. + * `out` may be NULL when output is not needed. + * + * The caller builds `command` as a string using snprintf(). + * Any embedded path literals must use single quotes so they survive + * the outer double-quote wrapping passed to cmd.exe. + * + * Returns 0 on success, -1 on failure. + */ +static int windows_ps(const char* ps_command, char* out, size_t out_size) +{ + char cmd[PS_CMD_MAX]; + FILE* fp; + int ret; + + ret = snprintf(cmd, sizeof(cmd), "powershell.exe -NoProfile -NonInteractive -Command \"%s\" 2>nul", ps_command); + if (ret < 0 || ret >= (int)sizeof(cmd)) { + errno = EINVAL; + return -1; + } + + fp = _popen(cmd, "r"); + if (!fp) { + windows_errno(GetLastError()); + log_error(errno, "Failed to run PowerShell command '%s' (from popen).\n", cmd); + return -1; + } + + if (out && out_size > 0) { + /* get the first line */ + if (!fgets(out, (int)out_size, fp)) { + out[0] = 0; + } else { + /* trim spaces and newlines */ + strtrim(out); + } + } + + ret = _pclose(fp); + if (ret == -1) { + errno = EINVAL; + log_error(errno, "Failed to run PowerShell command '%s' (from pclose).\n", cmd); + return -1; + } + if (ret != 0) { + errno = EINVAL; + log_error(errno, "PowerShell command '%s' failed with '%d' (from pclose).\n", cmd, ret); + return -1; + } + + return 0; +} + +/** + * Validates a GUID string of the form: + * xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + * Does NOT accept braced forms like {xxxxxxxx-...}. + * + * Returns true if valid, false otherwise. + */ +int windows_guid_is_valid(const char* guid, int len) +{ + if (!guid || len != 38) + return -1; + + if (guid[0] != '{' || guid[37] != '}') + return -1; + + const char* inner = guid + 1; + + for (int i = 0; i < 36; ++i) { + if (i == 8 || i == 13 || i == 18 || i == 23) { + if (inner[i] != '-') + return -1; + } else { + char c = inner[i]; + int is_digit = (c >= '0' && c <= '9'); + int is_upper_hex = (c >= 'A' && c <= 'F'); + int is_lower_hex = (c >= 'a' && c <= 'f'); + + if (!is_digit && !is_upper_hex && !is_lower_hex) + return -1; + } + } + + return 0; +} + +static int windows_read_guid(const char* guid_link, char* guid, size_t guid_size) +{ + int len = windows_readlink(guid_link, guid, guid_size); + if (len < 0 && errno == ENOENT) { + guid[0] = 0; + return 0; + } + if (len < 0) { + log_error(errno, "Error readlink '%s'. %s.\n", guid_link, strerror(errno)); + return -1; + } + + if ((size_t)len >= guid_size) { + errno = ENAMETOOLONG; + log_error(errno, "GUID too long in '%s'.\n", guid_link); + return -1; + } + + guid[len] = 0; + + /* validate that the GUID contains only safe characters: [A-Za-z0-9-{}] */ + if (windows_guid_is_valid(guid, len) != 0) + return -1; + + return 0; +} + +static int windows_rebuild_link(const struct fssnapshot_struct* fss, const char* name) +{ + char cmd[PS_CMD_MAX]; + char device_path[PATH_MAX]; + char target_path[PATH_MAX]; + char link_path[PATH_MAX]; + char guid_link[PATH_MAX]; + char guid[PATH_MAX]; + + pathcpy(guid_link, sizeof(guid_link), fss->snapshot_dir); + pathcat(guid_link, sizeof(guid_link), name); + pathcat(guid_link, sizeof(guid_link), SNAPSHOT_GUID); + + pathcpy(link_path, sizeof(link_path), fss->snapshot_dir); + pathcat(link_path, sizeof(link_path), name); + + int ret = windows_read_guid(guid_link, guid, sizeof(guid)); + if (ret != 0) { + return -1; + } + + /* if no GUID, assume it absent */ + if (guid[0] == 0) { + /* remove stale links */ + windows_rmdir(link_path); + return 0; + } + + snprintf(cmd, sizeof(cmd), + "(Get-WmiObject Win32_ShadowCopy | Where-Object {$_.ID -eq '%s'}).DeviceObject", + guid); + + if (windows_ps(cmd, device_path, sizeof(device_path)) != 0) { + log_error(errno, "Error getting DeviceObject from GUID '%s'. %s.\n", guid, strerror(errno)); + return -1; + } + + /* if no device path, assume snapshot absent */ + if (device_path[0] == 0) { + /* remove stale links */ + windows_rmdir(guid_link); + windows_rmdir(link_path); + return 0; + } + + pathimport(target_path, sizeof(target_path), device_path); + pathslash(target_path, sizeof(target_path)); + + /* remove stale link if present */ + windows_rmdir(link_path); + + if (windows_symlink_directory(target_path, link_path) != 0) { + log_error(errno, "Error creating symlink '%s'. %s.\n", link_path, strerror(errno)); + return -1; + } + + return 0; +} + +static int windows_delete_link(const struct fssnapshot_struct* fss, const char* name) +{ + char link_path[PATH_MAX]; + + pathcpy(link_path, sizeof(link_path), fss->snapshot_dir); + pathcat(link_path, sizeof(link_path), name); + + /* remove link if present */ + windows_rmdir(link_path); + + return 0; +} + +int fssnapshot_mount(const char* dir, struct fssnapshot_struct* fss) +{ + wchar_t conv_buf_vol[CONV_MAX]; + char conv_buf_root[CONV_MAX]; + wchar_t volume_root[PATH_MAX]; + wchar_t volume_name[PATH_MAX]; + wchar_t fs_name[32]; + uint32_t magic; + + /* + * GetVolumePathNameW() accepts any path: file, directory, or deep + * subdirectory, and resolves it to the volume root (e.g. L"C:\"). + * The result always carries a trailing backslash. + * + * Use convert_if_required() to avoid the automatic addition of \\?\ + * made by convert() that is propagated in the resulting volume_root + */ + if (!GetVolumePathNameW(convert_if_required(conv_buf_vol, dir), volume_root, PATH_MAX)) { + windows_errno(GetLastError()); + log_error(errno, "Error getting VolumeRoot from '%s'. %s.\n", dir, strerror(errno)); + return -1; + } + + if (!GetVolumeInformationW(volume_root, 0, 0, 0, 0, 0, fs_name, 32)) { + windows_errno(GetLastError()); + log_error(errno, "Error getting information of VolumeRoot '%s'. %s.\n", u16tou8(conv_buf_root, volume_root), strerror(errno)); + return -1; + } + if (wcscmp(fs_name, L"NTFS") == 0) + magic = WINDOWS_NTFS_MAGIC; + else + return -1; /* support only NTFS */ + + /* + * Obtain the canonical volume name: \\?\Volume{GUID}\ + * + * GetVolumePathNameW() may return a drive letter path ("C:\") or a + * directory mount point ("D:\Mount\"). Neither is guaranteed to be + * accepted by Win32_ShadowCopy.Create() on volumes without a drive + * letter. GetVolumeNameForVolumeMountPointW() always returns the + * stable kernel-form GUID path that VSS requires, regardless of how + * many (or how few) mount points the volume has. + * + * We store it in fss->dataset. + */ + if (!GetVolumeNameForVolumeMountPointW(volume_root, volume_name, PATH_MAX)) { + windows_errno(GetLastError()); + log_error(errno, "Error getting VolumeName of VolumeRoot '%s'. %s.\n", u16tou8(conv_buf_root, volume_root), strerror(errno)); + return -1; + } + + /* don't use pathimport for dataset to keep backslashes */ + pathcpy(fss->dataset, sizeof(fss->dataset), u16tou8(conv_buf_root, volume_name)); + + /* use pathimport to convert backslashes to slashes */ + pathimport(fss->root_dir, sizeof(fss->root_dir), u16tou8(conv_buf_root, volume_root)); + + /* the returned root_dir should match the start of the passed dir */ + if (pathncmp(fss->root_dir, dir, strlen(fss->root_dir)) != 0) { + errno = EINVAL; + log_error(errno, "Not matching VolumeRoot '%s' for '%s'.\n", fss->root_dir, dir); + return -1; + } + + pathcpy(fss->snapshot_dir, sizeof(fss->snapshot_dir), fss->root_dir); + pathcat(fss->snapshot_dir, sizeof(fss->snapshot_dir), SNAPSHOT_CONTAINER "/"); + + if (windows_mkdir(fss->snapshot_dir) != 0 && errno != EEXIST) { + log_error(errno, "Error creating directory '%s'. %s.\n", fss->snapshot_dir, strerror(errno)); + return -1; + } + + /* refresh the links, after reboots they gets invalidated */ + if (windows_rebuild_link(fss, SNAPSHOT_PENDING) != 0) { + return -1; + } + + if (windows_rebuild_link(fss, SNAPSHOT_STABLE) != 0) { + return -1; + } + + fss->magic = magic; + + return 0; +} + +int fssnapshot_stat(struct fssnapshot_struct* fss, const char* name, struct stat* st) +{ + char link_path[PATH_MAX]; + + pathcpy(link_path, sizeof(link_path), fss->snapshot_dir); + pathcat(link_path, sizeof(link_path), name); + pathcat(link_path, sizeof(link_path), SNAPSHOT_GUID); /* we check the GUID as it's the one that matter */ + + /* use lstat because we want to check the link */ + if (windows_lstat(link_path, st) != 0) + return -1; + + /* it must be a link to a directory */ + if (!S_ISLNKDIR(st->st_mode)) + return -1; + + return 0; +} + +int fssnapshot_create(const struct fssnapshot_struct* fss, const char* name) +{ + char cmd[PS_CMD_MAX]; + char device_path[PATH_MAX]; + char target_path[PATH_MAX]; + char link_path[PATH_MAX]; + char guid_link[PATH_MAX]; + char out[PATH_MAX]; + + /* create VSS shadow copy and capture the system-assigned GUID */ + snprintf(cmd, sizeof(cmd), + "$r = (Get-WmiObject -List Win32_ShadowCopy).Create('%s', 'ClientAccessible'); " + "if ($r.ReturnValue -eq 0) { " + "Write-Output ('ID_' + $r.ShadowID); " + "} else { " + "Write-Output ('0x{0:X8}' -f $r.ReturnValue); " + "}", + fss->dataset); + if (windows_ps(cmd, out, sizeof(out)) != 0) { + log_error(errno, "Error creating snapshot of VolumeName '%s'. %s.\n", fss->dataset, strerror(errno)); + return -1; + } + if (strncmp(out, "ID_", 3) != 0) { + errno = ENODATA; + log_error(errno, "VSS snapshot creation of '%s' failed with error %s.\n", fss->dataset, out); + return -1; + } + + const char* guid = out + 3; + + /* resolve the shadow copy's device object path */ + snprintf(cmd, sizeof(cmd), "(Get-WmiObject Win32_ShadowCopy | Where-Object {$_.ID -eq '%s'}).DeviceObject", guid); + if (windows_ps(cmd, device_path, sizeof(device_path)) != 0) { + log_error(errno, "Error getting the snapshot DeviceObject from GUID '%s'. %s.\n", guid, strerror(errno)); + goto bail_and_delete; + } + if (device_path[0] == 0) { + errno = ENODATA; + log_error(errno, "Empty snapshot DeviceObject of GUID '%s'. %s.\n", guid, strerror(errno)); + goto bail_and_delete; + } + + /* use pathimport to convert backslashes to slashes */ + pathimport(target_path, sizeof(target_path), device_path); + + /* the ending slash is required, otherwise the link won't work */ + pathslash(target_path, sizeof(target_path)); + + pathcpy(link_path, sizeof(link_path), fss->snapshot_dir); + pathcat(link_path, sizeof(link_path), name); + + /* remove potential stale link */ + windows_rmdir(link_path); + + int ret = windows_symlink_directory(target_path, link_path); + if (ret != 0) { + log_error(errno, "Error creating symlink '%s'. %s.\n", link_path, strerror(errno)); + goto bail_and_delete; + } + + pathcpy(guid_link, sizeof(guid_link), fss->snapshot_dir); + pathcat(guid_link, sizeof(guid_link), name); + pathcat(guid_link, sizeof(guid_link), SNAPSHOT_GUID); + + /* store GUID persistently as symlink target */ + ret = windows_symlink_directory(guid, guid_link); + if (ret != 0) { + log_error(errno, "Error creating GUID symlink '%s'. %s.\n", guid_link, strerror(errno)); + windows_rmdir(link_path); + goto bail_and_delete; + } + + return 0; + +bail_and_delete: + /* destroy the shadow copy we just created */ + snprintf(cmd, sizeof(cmd), "Get-WmiObject Win32_ShadowCopy | Where-Object {$_.ID -eq '%s'} | Remove-WmiObject", guid); + windows_ps(cmd, 0, 0); + return -1; +} + +int fssnapshot_delete(const struct fssnapshot_struct* fss, const char* name) +{ + char guid[PATH_MAX]; + char cmd[PS_CMD_MAX]; + char guid_link[PATH_MAX]; + + pathcpy(guid_link, sizeof(guid_link), fss->snapshot_dir); + pathcat(guid_link, sizeof(guid_link), name); + pathcat(guid_link, sizeof(guid_link), SNAPSHOT_GUID); + + if (windows_read_guid(guid_link, guid, sizeof(guid)) != 0) + return -1; + + /* if the GUID link is gone, assume everything is gone */ + if (guid[0] == 0) { + /* remove as not valid anymore */ + windows_delete_link(fss, name); + return 0; + } + + /* destroy the VSS shadow copy */ + snprintf(cmd, sizeof(cmd), "Get-WmiObject Win32_ShadowCopy | Where-Object {$_.ID -eq '%s'} | Remove-WmiObject", guid); + int ret = windows_ps(cmd, 0, 0); + if (ret != 0) { + log_error(errno, "Error destroy the snapshot from GUID '%s'. %s.\n", guid, strerror(errno)); + /* remove as not valid anymore */ + windows_delete_link(fss, name); + return -1; + } + + /* remove live symlink if present */ + windows_delete_link(fss, name); + + /* remove persistent GUID symlink */ + if (windows_rmdir(guid_link) != 0) { + log_error(errno, "Error rmdir '%s'. %s.\n", guid_link, strerror(errno)); + return -1; + } + + return 0; +} + +int fssnapshot_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name) +{ + char old_guid[PATH_MAX]; + char new_guid[PATH_MAX]; + + pathcpy(old_guid, sizeof(old_guid), fss->snapshot_dir); + pathcat(old_guid, sizeof(old_guid), old_name); + pathcat(old_guid, sizeof(old_guid), SNAPSHOT_GUID); + pathcpy(new_guid, sizeof(new_guid), fss->snapshot_dir); + pathcat(new_guid, sizeof(new_guid), new_name); + pathcat(new_guid, sizeof(new_guid), SNAPSHOT_GUID); + + if (windows_rename(old_guid, new_guid) != 0) { + log_error(errno, "Error renaming '%s' to '%s'. %s.\n", old_guid, new_guid, strerror(errno)); + return -1; + } + + /* the rename is the latest operation, and then we don't care about the links, we just remove both */ + windows_delete_link(fss, old_name); + windows_delete_link(fss, new_name); + + return 0; +} + +void fssnapshot_unmount(const struct fssnapshot_struct* fss) +{ + /* + * Remove symlinks on exit to prevent "Dangling Links." + * Since \Device\HarddiskVolumeShadowCopyN paths are reassigned by the kernel + * at boot, a stale link would either point to a non-existent device or, + * worse, a different disk's snapshot. + * + * Deleting the link ensures system hygiene and prevents users from seeing + * "Location not available" errors or incorrect data after a reboot. + */ + windows_delete_link(fss, SNAPSHOT_PENDING); + windows_delete_link(fss, SNAPSHOT_STABLE); +} + /****************************************************************************/ /* resolve */ @@ -425,15 +1017,15 @@ static void devattr_property(HANDLE h, char* serial, char* model, char* inter) case BusTypeiScsi : type = "iSCSI"; break; case BusTypeSas : type = "SAS"; break; case BusTypeSata : type = "SATA"; break; - case BusTypeSd : type = "SD"; break; - case BusTypeMmc : type = "MMC"; break; + case BusTypeSd : type = "SD"; break; /* Secure Digital cards */ + case BusTypeMmc : type = "MMC"; break; /* NAND Embedded MultiMediaCards (eMMC) */ case BusTypeVirtual : type = "Virtual"; break; case BusTypeFileBackedVirtual : type = "Virtual"; break; case BusTypeSpaces : type = "Storage Spaces"; break; - case BusTypeNvme : type = "NVMe"; break; - case BusTypeSCM : type = "SCM"; break; - case BusTypeUfs : type = "UFS"; break; - case BusTypeNvmeof : type = "NVMe"; break; + case BusTypeNvme : type = "NVMe"; break; /* NAND */ + case BusTypeSCM : type = "SCM"; break; /* NAND Storage Class Memory */ + case BusTypeUfs : type = "UFS"; break; /* NAND Universal Flash Storage */ + case BusTypeNvmeof : type = "NVMe"; break; /* NAND NVMe over Fabrics */ default : } if (inter && type) /* always override interface as more detailed than smartctl */ diff --git a/cmdline/mkstream.c b/cmdline/mkstream.c index c7a38050..71068cbe 100644 --- a/cmdline/mkstream.c +++ b/cmdline/mkstream.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2015 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "stream.h" #include "support.h" diff --git a/cmdline/mktest.c b/cmdline/mktest.c index 8a898a32..41079928 100644 --- a/cmdline/mktest.c +++ b/cmdline/mktest.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" diff --git a/cmdline/os.h b/cmdline/os.h deleted file mode 100644 index eacae08e..00000000 --- a/cmdline/os.h +++ /dev/null @@ -1,160 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2026 Andrea Mazzoleni - -#ifndef __OS_H -#define __OS_H - -/****************************************************************************/ -/* signal */ - -/** - * Enable or disable signal handling. - * @param enable 1 to enable signals, 0 to disable - */ -void os_signal_set(int enable); - -/** - * Initialize signal handling. - */ -void os_signal_init(void (*handler_term)(int sig), void (*handler_hup)(int sig)); - -/** - * Restore signal handlers after fork in child process. - * This resets signals to default handling. - */ -void os_signal_restore_after_fork(void); - -/****************************************************************************/ -/* os */ - -/** - * Initializes the system. - */ -void os_init(int opt); - -/** - * Deinitializes the system. - */ -void os_done(void); - -/** - * Get the os_tick counter value. - * - * Note that the frequency is unspecified, because the time measure - * is meant to be used to compare the ratio between usage times. - */ -uint64_t os_tick(void); - -/** - * Get the os_tick counter value in millisecond. - */ -uint64_t os_tick_ms(void); - -/** - * Abort the process with a stacktrace. - */ -void os_abort(void) __noreturn; - -/** - * Clear the screen. - */ -void os_clear(void); - -/** - * Opaque handle tracking a process execution stream and its process ID. - * - * This structure encapsulates a standard buffered I/O stream tied to the - * redirected standard output of a child process spawned via os_popen(). - */ -#ifdef __MINGW32__ -#define OS_FILE FILE -#else -typedef struct OS_FILE { - FILE* fp; /**< Standard I/O buffered stream wrapper. */ - pid_t pid; /**< Process ID of the child process. */ -} OS_FILE; -#endif - -/** - * Spawns a child process and opens a buffered stream to read its stdout. - * - * This function acts as a safer alternative to standard popen(), accepting an - * explicit argument vector rather than evaluating a shell command string. - * - * The child process is completely isolated in its own process group (via setpgid) - * with its stdin and stderr redirected to /dev/null. The underlying pipe is - * created with O_CLOEXEC set to prevent leakage across concurrent forks. - * - * @param argv A NULL-terminated array of strings representing the argument vector. - * argv[0] must contain the absolute path to the verified executable. - * @return A pointer to an initialized OS_FILE structure on success, or NULL on failure. - * On failure, errno is set appropriately. - */ -OS_FILE* os_popen(const char** argv); - -/** - * Reads a line from an OS execution stream. - * - * Reads characters from the specified stream into the buffer string until - * (size - 1) characters are read, a newline character is read and transferred, - * or an end-of-file (EOF) condition is encountered. A terminating null - * character is appended. - * - * @param s Pointer to an array of chars where the string read is stored. - * @param size Maximum number of characters to be read (including the null character). - * @param stream Pointer to the OS_FILE tracking context. - * - * @return On success, returns the buffer pointer @p s. If EOF is reached or a read - * error occurs before any characters are read, returns NULL. - */ -char* os_fgets(char* s, int size, OS_FILE* stream); - -/** - * Closes an execution stream and reaps the associated child process. - * - * Closes the underlying buffered standard I/O stream, releases the allocated - * tracking context container, and blocks until the associated child process - * terminates to prevent the creation of zombie processes. - * - * @param stream Pointer to the active OS_FILE context to close and free. - * - * @return The termination status of the child process on success, or -1 on failure. - * On failure, errno is set appropriately. - */ -int os_pclose(OS_FILE* stream); - -/* - * Fork and execute a verified executable, discarding all I/O. - * - * Spawns @argv[0] in a new process with stdin, stdout and stderr all - * redirected to /dev/null. Use this for fire-and-forget tasks where the - * child's output is not needed. - * - * The child is placed in its own process group (setpgid) to isolate it - * from signals sent to the daemon's process group. - * - * @argv NULL-terminated argument vector. argv[0] must be the absolute path - * to the executable. - * - * Returns the child execit status on success, or -1 on failure. - */ -int os_spawn_and_wait(const char** argv); - -/** - * Get the optimal CPU for the speed test - */ -int os_get_optimal_cpu(void); - -/** - * Fill memory with pseudo-random values - */ -int os_randomize(void* ptr, size_t size); - -/** - * Validates a string for exec. - * Returns -1 if dangerous characters are detected, 0 otherwise. - */ -int os_validate_exec_input(const char* str); - -#endif - diff --git a/cmdline/parity.c b/cmdline/parity.c index 2fa02002..28df8dd9 100644 --- a/cmdline/parity.c +++ b/cmdline/parity.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "elem.h" diff --git a/cmdline/pool.c b/cmdline/pool.c index dc66ded9..7027d59b 100644 --- a/cmdline/pool.c +++ b/cmdline/pool.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2013 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "elem.h" diff --git a/cmdline/rehash.c b/cmdline/rehash.c index 0664d372..24eb4687 100644 --- a/cmdline/rehash.c +++ b/cmdline/rehash.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2013 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "util.h" #include "elem.h" diff --git a/cmdline/scan.c b/cmdline/scan.c index 2ba82a8c..71f4dbd6 100644 --- a/cmdline/scan.c +++ b/cmdline/scan.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "elem.h" diff --git a/cmdline/scrub.c b/cmdline/scrub.c index 91860385..093c9d95 100644 --- a/cmdline/scrub.c +++ b/cmdline/scrub.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2013 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "elem.h" diff --git a/cmdline/search.c b/cmdline/search.c index 3187094a..8adc58ec 100644 --- a/cmdline/search.c +++ b/cmdline/search.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2014 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "search.h" diff --git a/cmdline/selftest.c b/cmdline/selftest.c index 8cc31076..8ab1a437 100644 --- a/cmdline/selftest.c +++ b/cmdline/selftest.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "snapraid.h" #include "util.h" diff --git a/cmdline/snapraid.c b/cmdline/snapraid.c index 92c38ad2..d4b4e5f6 100644 --- a/cmdline/snapraid.c +++ b/cmdline/snapraid.c @@ -1,9 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" -#include "os.h" #include "snapraid.h" #include "support.h" #include "elem.h" @@ -1413,7 +1412,7 @@ int snapraid_main(int argc, char* argv[]) } } - os_init(opt.force_scan_winfind); + os_init(OS_INIT_OPT_AVOID_SLEEP | (opt.force_scan_winfind ? OS_INIT_OPT_WINFIND : 0)); app_init(); raid_init(); crc32c_init(); @@ -2092,19 +2091,19 @@ int snapraid_main(int argc, char* argv[]) exit(EXIT_FAILURE); /* LCOV_EXCL_STOP */ } - if (app_global_interrupt()) { + if (os_signal_interrupt()) { /* LCOV_EXCL_START */ #ifdef _WIN32 exit(STATUS_CONTROL_C_EXIT); #else /* restore default handler */ - signal(app_global_interrupt(), SIG_DFL); + signal(os_signal_interrupt(), SIG_DFL); /* raise the signal again*/ - raise(app_global_interrupt()); + raise(os_signal_interrupt()); /* if raise() didn't terminate */ - _exit(128 + app_global_interrupt()); + _exit(128 + os_signal_interrupt()); #endif /* LCOV_EXCL_STOP */ } diff --git a/cmdline/speed.c b/cmdline/speed.c index 4cff4e0f..2754c3e7 100644 --- a/cmdline/speed.c +++ b/cmdline/speed.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "snapraid.h" #include "util.h" diff --git a/cmdline/state.c b/cmdline/state.c index ad291db6..515ecf75 100644 --- a/cmdline/state.c +++ b/cmdline/state.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "elem.h" #include "import.h" @@ -5050,7 +5050,7 @@ int state_progress_begin(struct snapraid_state* state, block_off_t blockstart, b return -1; /* stop if requested */ - if (app_global_interrupt()) { + if (os_signal_interrupt()) { /* LCOV_EXCL_START */ if (!state->opt.gui) { msg_status("Not starting due to interruption\n"); @@ -5571,7 +5571,7 @@ int state_progress(struct snapraid_state* state, struct snapraid_io* io, block_o } /* stop if requested */ - if (app_global_interrupt()) { + if (os_signal_interrupt()) { /* LCOV_EXCL_START */ if (!state->opt.gui) { msg_bar("\n"); diff --git a/cmdline/status.c b/cmdline/status.c index 7754d808..27ad87fb 100644 --- a/cmdline/status.c +++ b/cmdline/status.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2013 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "elem.h" diff --git a/cmdline/stream.c b/cmdline/stream.c index 94c5c066..e4b8b40d 100644 --- a/cmdline/stream.c +++ b/cmdline/stream.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "util.h" diff --git a/cmdline/support.c b/cmdline/support.c index df83bbb1..074d1174 100644 --- a/cmdline/support.c +++ b/cmdline/support.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" @@ -362,10 +362,18 @@ static void vmsg(FILE* out, const char* format, va_list ap) free(dup); } -void log_fatal(int err, const char* format, ...) +static void msg(FILE* out, const char* format, ...) __attribute__((format(attribute_printf, 2, 3))); +static void msg(FILE* out, const char* format, ...) { va_list ap; + va_start(ap, format); + vmsg(out, format, ap); + va_end(ap); +} + +void vlog_fatal(int err, const char* format, va_list ap, const char* post) +{ lock_msg(); if (stdlog) { @@ -374,26 +382,41 @@ void log_fatal(int err, const char* format, ...) else fprintf(stdlog, "msg:fatal: "); - va_start(ap, format); - vfprintf(stdlog, format, ap); - va_end(ap); + va_list ap_copy; + va_copy(ap_copy, ap); + vfprintf(stdlog, format, ap_copy); + va_end(ap_copy); + + if (post) + fprintf(stdlog, "%s", post); fflush(stdlog); } - va_start(ap, format); - vmsg(stderr, format, ap); - va_end(ap); + va_list ap_copy2; + va_copy(ap_copy2, ap); + vmsg(stderr, format, ap_copy2); + va_end(ap_copy2); + + if (post) + msg(stderr, "%s", post); fflush(stderr); unlock_msg(); } -void log_error(int err, const char* format, ...) +void log_fatal(int err, const char* format, ...) { va_list ap; + va_start(ap, format); + vlog_fatal(err, format, ap, 0); + va_end(ap); +} + +void vlog_error(int err, const char* format, va_list ap, const char* post) +{ lock_msg(); if (stdlog) { @@ -402,15 +425,23 @@ void log_error(int err, const char* format, ...) else fprintf(stdlog, "msg:error: "); - va_start(ap, format); - vfprintf(stdlog, format, ap); - va_end(ap); + va_list ap_copy; + va_copy(ap_copy, ap); + vfprintf(stdlog, format, ap_copy); + va_end(ap_copy); + + if (post) + fprintf(stdlog, "%s", post); fflush(stdlog); } else { - va_start(ap, format); - vmsg(stderr, format, ap); - va_end(ap); + va_list ap_copy; + va_copy(ap_copy, ap); + vmsg(stderr, format, ap_copy); + va_end(ap_copy); + + if (post) + msg(stderr, "%s", post); fflush(stderr); } @@ -418,11 +449,17 @@ void log_error(int err, const char* format, ...) unlock_msg(); } - -void log_info(int err, const char* format, ...) +void log_error(int err, const char* format, ...) { va_list ap; + va_start(ap, format); + vlog_error(err, format, ap, 0); + va_end(ap); +} + +void vlog_info(int err, const char* format, va_list ap, const char* post) +{ lock_msg(); if (stdlog) { @@ -431,15 +468,23 @@ void log_info(int err, const char* format, ...) else fprintf(stdlog, "msg:info: "); - va_start(ap, format); - vfprintf(stdlog, format, ap); - va_end(ap); + va_list ap_copy; + va_copy(ap_copy, ap); + vfprintf(stdlog, format, ap_copy); + va_end(ap_copy); + + if (post) + fprintf(stdlog, "%s", post); fflush(stdlog); } else { - va_start(ap, format); - vmsg(stderr, format, ap); - va_end(ap); + va_list ap_copy; + va_copy(ap_copy, ap); + vmsg(stderr, format, ap_copy); + va_end(ap_copy); + + if (post) + msg(stderr, "%s", post); fflush(stderr); } @@ -447,6 +492,36 @@ void log_info(int err, const char* format, ...) unlock_msg(); } +void log_info(int err, const char* format, ...) +{ + va_list ap; + + va_start(ap, format); + vlog_info(err, format, ap, 0); + va_end(ap); +} + +void os_syslog(int level, const char* format, ...) +{ + va_list ap; + + va_start(ap, format); + switch (level) { + case OS_LVL_CRITICAL : + vlog_fatal(ESYSLOG, format, ap, "\n"); + break; + case OS_LVL_ERROR : + vlog_error(ESYSLOG, format, ap, "\n"); + break; + case OS_LVL_WARNING : + case OS_LVL_INFO : + /* do not log */ + break; + } + + va_end(ap); +} + void log_expected(int err, const char* format, ...) { va_list ap; diff --git a/cmdline/support.h b/cmdline/support.h index 569e2f1f..9900a6e1 100644 --- a/cmdline/support.h +++ b/cmdline/support.h @@ -4,7 +4,6 @@ #ifndef __SUPPORT_H #define __SUPPORT_H -#include "os.h" #include "app.h" /****************************************************************************/ @@ -93,6 +92,7 @@ typedef void log_ptr(int err, const char* format, ...) __attribute__((format(att * * These messages go in the log file and in stderr unconditionally. */ +void vlog_fatal(int err, const char* format, va_list ap, const char* post); void log_fatal(int err, const char* format, ...) __attribute__((format(attribute_printf, 2, 3))); /** @@ -106,6 +106,7 @@ void log_fatal(int err, const char* format, ...) __attribute__((format(attribute * * These messages go in the log file if specified, otherwise they go in stderr. */ +void vlog_error(int err, const char* format, va_list ap, const char* post); void log_error(int err, const char* format, ...) __attribute__((format(attribute_printf, 2, 3))); /** diff --git a/cmdline/sync.c b/cmdline/sync.c index b202b2c4..b8f34f87 100644 --- a/cmdline/sync.c +++ b/cmdline/sync.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "elem.h" diff --git a/cmdline/thermal.c b/cmdline/thermal.c index dc6ff583..17bdc749 100644 --- a/cmdline/thermal.c +++ b/cmdline/thermal.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2025 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "thermal.h" #include "state.h" @@ -328,7 +328,7 @@ void state_thermal_cooldown(struct snapraid_state* state) sleep_time -= 30; } - if (!app_global_interrupt()) { /* don't wake-up if we are interrupting */ + if (!os_signal_interrupt()) { /* don't wake-up if we are interrupting */ log_tag("thermal:spinup\n"); /* spinup */ diff --git a/cmdline/touch.c b/cmdline/touch.c index b32cad2d..1e22a852 100644 --- a/cmdline/touch.c +++ b/cmdline/touch.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2014 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "elem.h" diff --git a/cmdline/unix.c b/cmdline/unix.c deleted file mode 100644 index 087e91ef..00000000 --- a/cmdline/unix.c +++ /dev/null @@ -1,832 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2013 Andrea Mazzoleni - -#include "portable.h" - -#ifndef __MINGW32__ /* Only for Unix */ - -#include "os.h" -#include "support.h" - -/****************************************************************************/ -/* signal */ - -void os_signal_restore_after_fork(void) -{ - struct sigaction sa; - - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = SIG_DFL; - sigemptyset(&sa.sa_mask); - sa.sa_flags = 0; - - sigaction(SIGTERM, &sa, 0); - sigaction(SIGINT, &sa, 0); - sigaction(SIGHUP, &sa, 0); - /* do not restore SIGPIPE */ - - /* ensure signals are unblocked */ - sigset_t mask; - sigemptyset(&mask); - sigprocmask(SIG_SETMASK, &mask, NULL); /* cannot use pthread_sigmask after fork */ -} - -void os_signal_init(void (*handler_term)(int sig), void (*handler_hup)(int sig)) -{ - struct sigaction sa; - - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = handler_term; - sigemptyset(&sa.sa_mask); - sa.sa_flags = SA_RESTART; /* use the SA_RESTART to automatically restart interrupted system calls */ - - sigaction(SIGTERM, &sa, 0); - sigaction(SIGINT, &sa, 0); - - sa.sa_handler = handler_hup; - sigemptyset(&sa.sa_mask); - sa.sa_flags = SA_RESTART; /* use the SA_RESTART to automatically restart interrupted system calls */ - - sigaction(SIGHUP, &sa, 0); - - sa.sa_handler = SIG_IGN; /* ignore the signal */ - sigemptyset(&sa.sa_mask); - sa.sa_flags = 0; - sigaction(SIGPIPE, &sa, 0); -} - -/****************************************************************************/ -/* unix */ - -#if !HAVE_EACCESS -int eaccess(const char* pathname, int mode) -{ - return faccessat(AT_FDCWD, pathname, mode, AT_EACCESS); -} -#endif - -#if HAVE_POSIX_FADVISE -int posix_fadvise_wrapper(int fd, off_t offset, off_t len, int advice) -{ - int ret = posix_fadvise(fd, offset, len, advice); - - if (ret == ENOENT) - return ENOSYS; - - return ret; -} -#endif - -int open_noatime(const char* file, int flags) -{ -#ifdef O_NOATIME - int f = open(file, flags | O_NOATIME); - - /* only root is allowed to use O_NOATIME, in case retry without it */ - if (f == -1 && errno == EPERM) - f = open(file, flags); - return f; -#else - return open(file, flags); -#endif -} - -int dirent_hidden(struct dirent* dd) -{ - return dd->d_name[0] == '.'; -} - -size_t direct_size(void) -{ - long size; - - size = sysconf(_SC_PAGESIZE); - - if (size == -1) { - /* LCOV_EXCL_START */ - log_fatal(EEXTERNAL, "No page size\n"); - exit(EXIT_FAILURE); - /* LCOV_EXCL_STOP */ - } - - return size; -} - -const char* stat_desc(struct stat* st) -{ - if (S_ISREG(st->st_mode)) - return "regular"; - if (S_ISDIR(st->st_mode)) - return "directory"; - if (S_ISCHR(st->st_mode)) - return "character"; - if (S_ISBLK(st->st_mode)) - return "block-device"; - if (S_ISFIFO(st->st_mode)) - return "fifo"; - if (S_ISLNK(st->st_mode)) - return "symlink"; - if (S_ISSOCK(st->st_mode)) - return "socket"; - return "unknown"; -} - -/****************************************************************************/ -/* exec */ - -/* - * Securely verify and open an executable file. - * - * Performs a series of security checks on the file at @exec_path before - * returning an open file descriptor suitable for use with fexecve(2), or - * falling back to execve(2) on systems that lack it. - * - * Security model: - * - @exec_path must be absolute. - * - The path is resolved via realpath(3) to canonicalize it and eliminate - * symlinks before any further checks are performed. - * - The parent directory is opened with O_PATH | O_NOFOLLOW to pin its - * inode, and the file is opened with openat(2) relative to that pinned - * descriptor. This closes the TOCTOU window between path resolution - * and file open. - * - Both the parent directory and the file must be owned by root or the - * daemon's real/effective UID, must not be world-writable, and must not - * be group-writable by a group other than the daemon's real/effective GID. - * - The file must be a regular file with at least one execute bit set. - * - The setuid and setgid bits must not be set. - * - On systems with fexecve(2) support, the returned fd is opened without - * O_CLOEXEC if the file is a script (@is_script=1). This is necessary because - * fexecve(2) executes scripts by passing a descriptor path (e.g., /dev/fd/N) - * to the script interpreter, which requires the fd to remain open across - * the execve syscall. For compiled binaries, O_CLOEXEC is safely applied - * to prevent leaking the fd to the spawned process. On systems without - * fexecve(2), execve(2) is used with @resolved_path and O_CLOEXEC is - * always applied. - * - * @exec_path Absolute path to the executable to verify. - * @resolved_path Caller-allocated buffer of at least PATH_MAX bytes. On - * success, filled with the canonicalized path from realpath(3). - * - * Returns an open file descriptor (>= 0) on success. The caller is - * responsible for closing it. Returns -1 on any verification failure; - * the specific reason is emitted via log_error(...). - */ -static int verify_executable(const char* exec_path, char* resolved_path, int is_script) -{ - struct stat st; - uid_t process_uid, process_euid; - gid_t process_gid, process_egid; - - process_uid = getuid(); - process_euid = geteuid(); - process_gid = getgid(); - process_egid = getegid(); - - /* verify path is absolute */ - if (exec_path[0] != '/') { - log_error(EINVAL, "Path %s must be absolute", exec_path); - return -1; - } - - /* resolve the path to prevent symlink attacks */ - if (!realpath(exec_path, resolved_path)) { - log_error(errno, "Failed to resolve %s. %s.", exec_path, strerror(errno)); - return -1; - } - - char* last_slash = strrchr(resolved_path, '/'); - if (last_slash == 0) { - log_error(EINVAL, "Relative execution of %s not allowed", resolved_path); - return -1; - } - if (last_slash == resolved_path) { - log_error(EINVAL, "Root dir execution of %s not allowed", resolved_path); - return -1; - } - - const char* exec_name = last_slash + 1; - if (exec_name[0] == 0) { - log_error(EINVAL, "No executable name in %s", exec_path); - return -1; - } - - char dir_path[PATH_MAX]; - size_t dir_len = last_slash - resolved_path; - memcpy(dir_path, resolved_path, dir_len); - dir_path[dir_len] = 0; - - int dir_fd = open(dir_path, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); - if (dir_fd < 0) { - log_error(errno, "Failed to open directory %s. %s.", dir_path, strerror(errno)); - return -1; - } - - if (fstat(dir_fd, &st) != 0) { - log_error(errno, "Failed to stat directory %s. %s.", dir_path, strerror(errno)); - close(dir_fd); - return -1; - } - - /* directory must be owned by root or the daemon's real user */ - if (st.st_uid != process_uid && st.st_uid != process_euid && st.st_uid != 0) { - log_error(EINVAL, "Directory %s owner must match the daemon owner or be root", dir_path); - close(dir_fd); - return -1; - } - - /* directory must be not group-writable unless group matches daemon */ - if ((st.st_mode & S_IWGRP) && st.st_gid != process_gid && st.st_gid != process_egid && st.st_gid != 0) { - log_error(EINVAL, "Directory %s must be not group-writable unless group matches daemon owner or root", dir_path); - close(dir_fd); - return -1; - } - - /* directory must be not world-writable */ - if (st.st_mode & S_IWOTH) { - log_error(EINVAL, "Directory %s must be not world-writable", dir_path); - close(dir_fd); - return -1; - } - - /* - * Open the executable - */ - int flags = O_RDONLY | O_NOFOLLOW; -#if HAVE_FEXECVE - if (!is_script) { - flags |= O_CLOEXEC; /* with fexecve cannot use O_CLOEXEC (Close on Exec) for scripts */ - } -#else - (void)is_script; - flags |= O_CLOEXEC; -#endif - - int fd = openat(dir_fd, exec_name, flags); - if (fd < 0) { - log_error(errno, "Failed to open %s. %s.", resolved_path, strerror(errno)); - close(dir_fd); - return -1; - } - - close(dir_fd); - - /* get the file handle (TOCTOU Protection) */ - if (fstat(fd, &st) == -1) { - log_error(errno, "Failed to stat %s. %s.", resolved_path, strerror(errno)); - close(fd); - return -1; - } - - /* ensure it's a regular file */ - if (!S_ISREG(st.st_mode)) { - log_error(EINVAL, "File %s is not a regular file", resolved_path); - close(fd); - return -1; - } - - /* explicitly reject scripts if not allowed */ - if (!is_script) { - char magic[2]; - if (read(fd, magic, 2) == 2 && magic[0] == '#' && magic[1] == '!') { - log_error(EINVAL, "File %s is a script, which is not supported", resolved_path); - close(fd); - return -1; - } - if (lseek(fd, 0, SEEK_SET) == (off_t)-1) { - /* ignore lseek errors on block devices or pipes, though it's a regular file here */ - } - } - - /* ensure it has execute permissions */ - if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) { - log_error(EINVAL, "File %s is not an executable", resolved_path); - close(fd); - return -1; - } - - /* must be owned by root or the daemon's real user */ - if (st.st_uid != process_uid && st.st_uid != process_euid && st.st_uid != 0) { - log_error(EINVAL, "File %s owner must match the daemon owner or be root", resolved_path); - close(fd); - return -1; - } - - /* must be not group-writable unless group matches daemon */ - if ((st.st_mode & S_IWGRP) && st.st_gid != process_gid && st.st_gid != process_egid && st.st_gid != 0) { - log_error(EINVAL, "File %s must be not group-writable unless group matches daemon owner or root", resolved_path); - close(fd); - return -1; - } - - /* must be not world-writable */ - if (st.st_mode & S_IWOTH) { - log_error(EINVAL, "File %s must be not world-writable", resolved_path); - close(fd); - return -1; - } - - /* must be not setuid / setgid */ - if (st.st_mode & (S_ISUID | S_ISGID)) { - log_error(EINVAL, "File %s has setuid/setgid bits set", resolved_path); - close(fd); - return -1; - } - - return fd; -} - -int os_validate_exec_input(const char* str) -{ - /* reject paths trying to go up levels */ - if (strstr(str, "..") != 0) - return -1; - - /* reject inputs starting with '-' to prevent Flag Injection */ - if (str[0] == '-') - return -1; - - return 0; -} - -/* - * Scrubbed environment - * Only provide the bare essentials. - */ -static char* const envp_scrubbed[] = { - "PATH=" -#ifdef __APPLE__ - "/opt/homebrew/bin:" -#endif - "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - "TERM=dumb", - "LANG=C", - "IFS= \t\n", - NULL -}; - -static int pipe_cloexec(int pipefd[2]) -{ -#ifdef HAVE_PIPE2 - return pipe2(pipefd, O_CLOEXEC); -#else - if (pipe(pipefd) < 0) - return -1; - - for (int i = 0; i < 2; i++) { - int flags = fcntl(pipefd[i], F_GETFD); - if (flags < 0) - goto bail; - - if (fcntl(pipefd[i], F_SETFD, flags | FD_CLOEXEC) < 0) - goto bail; - } - - return 0; - -bail: - close(pipefd[0]); - close(pipefd[1]); - return -1; -#endif -} - -/* - * Fork and execute a verified executable, capturing stdout. - * - * Spawns @argv[0] in a new process with stdout connected to a pipe whose - * read end is returned in @stdout_fd. stdin and stderr are redirected to - * /dev/null. Use this when the child's error output needs to be read and - * processed by the daemon. - * - * The child is placed in its own process group (setpgid) to isolate it - * from signals sent to the daemon's process group. The daemon can terminate - * the child and all its descendants with kill(-pid, SIGTERM). - * - * The pipe is created with O_CLOEXEC on both ends. The write end is closed - * in the parent after fork. The read end's buffer is reduced to 4096 bytes - * to improve read responsiveness on low-volume output. - * - * @argv NULL-terminated argument vector. argv[0] must be the absolute - * path to the executable. - * @stdout_fd On success, set to the read end of the stdout pipe. The caller - * is responsible for closing it when done. - * - * Returns the child PID on success, or -1 on failure. - */ -static pid_t os_spawn_stdout(const char** argv, int* stdout_fd) -{ - char resolved_path[PATH_MAX]; - int out_pipe[2]; - pid_t pid; - - int fd = verify_executable(argv[0], resolved_path, 0); - if (fd < 0) { - return -1; - } - - if (pipe_cloexec(out_pipe) < 0) { - close(fd); - return -1; - } - - pid = fork(); - if (pid < 0) { - close(out_pipe[0]); - close(out_pipe[1]); - close(fd); - return -1; - } - - if (pid == 0) { - /* child process */ - - /* - * Create a new process group for the child. - * This isolates the child from signals sent to the daemon's process group - * and allows the daemon to kill this process and all its future children - * (the entire group) using kill(-pid, SIGTERM). - */ - setpgid(0, 0); - - /* io sandboxing */ - int null_fd = open("/dev/null", O_RDWR | O_CLOEXEC); - if (null_fd < 0) - _exit(126); - - /* stdin -> /dev/null */ - /* stdout -> pipe */ - /* stderr -> /dev/null */ - if (dup2(null_fd, STDIN_FILENO) < 0 - || dup2(out_pipe[1], STDOUT_FILENO) < 0 - || dup2(null_fd, STDERR_FILENO) < 0) - _exit(126); - - close(out_pipe[0]); - close(out_pipe[1]); - - /* if the fd we opened is not one of the standard ones, close it */ - if (null_fd > STDERR_FILENO) - close(null_fd); - -#if defined(CLOSE_RANGE_CLOEXEC) && defined(HAVE_CLOSE_RANGE) - /* - * Set all fd to be closed on exec as extra safety measure - * - * fallback: if it fails, we assume to be still safe, as all fds and - * sockets should be already created with CLOEXEC. - */ - close_range(3, ~0U, CLOSE_RANGE_CLOEXEC); -#endif - - /* restore and unblock signals */ - os_signal_restore_after_fork(); - - /* use the resolved path for execution */ - argv[0] = resolved_path; - - /* - * Direct Execution via File Descriptor - * The kernel uses the shebang in the FD to find the interpreter. - */ -#if HAVE_FEXECVE - fexecve(fd, (char**)argv, envp_scrubbed); -#else - /* fallback: unfortunately must use the path */ - execve(resolved_path, (char**)argv, envp_scrubbed); -#endif - _exit(127); - } - - /* parent */ - close(fd); - -#ifdef F_SETPIPE_SZ /* not available on macOS */ - /* set the pipe buffer to the minimum to improve responsiveness */ - if (fcntl(out_pipe[0], F_SETPIPE_SZ, 4096) == -1) { - log_error(errno, "Failed to set pipe size. %s.", strerror(errno)); - /* log non-fatal error or ignore */ - } -#endif - - close(out_pipe[1]); - - *stdout_fd = out_pipe[0]; - return pid; -} - -static int os_wait(pid_t pid, int* status) -{ - int ret; - - do { - ret = waitpid(pid, status, 0); - } while (ret == -1 && errno == EINTR); - - return ret; -} - -int os_spawn_and_wait(const char** argv) -{ - char resolved_path[PATH_MAX]; - pid_t pid; - - int fd = verify_executable(argv[0], resolved_path, 0); - if (fd < 0) - return -1; - - pid = fork(); - if (pid < 0) { - log_error(errno, "Fork failed. %s.", strerror(errno)); - close(fd); - return -1; - } - - if (pid == 0) { - /* child process */ - /* - * Create a new process group for the child. - * This isolates the child from signals sent to the daemon's process group - * and allows the daemon to kill this process and all its future children - * (the entire group) using kill(-pid, SIGTERM). - */ - setpgid(0, 0); - - /* io sandboxing */ - int null_fd = open("/dev/null", O_RDWR | O_CLOEXEC); - if (null_fd < 0) - _exit(126); - - /* stdin -> /dev/null */ - /* stdout -> /dev/null */ - /* stderr -> /dev/null */ - if (dup2(null_fd, STDIN_FILENO) < 0 - || dup2(null_fd, STDOUT_FILENO) < 0 - || dup2(null_fd, STDERR_FILENO) < 0) - _exit(126); - - /* if the fd we opened is not one of the standard ones, close it */ - if (null_fd > STDERR_FILENO) - close(null_fd); - -#if defined(CLOSE_RANGE_CLOEXEC) && defined(HAVE_CLOSE_RANGE) - /* - * Set all fd to be closed on exec as extra safety measure - * - * fallback: if it fails, we assume to be still safe, as all fds and - * sockets should be already created with CLOEXEC. - */ - close_range(3, ~0U, CLOSE_RANGE_CLOEXEC); -#endif - - /* restore and unblock signals */ - os_signal_restore_after_fork(); - - /* use the resolved path for execution */ - argv[0] = resolved_path; - - /* - * Direct Execution via File Descriptor - * The kernel uses the shebang in the FD to find the interpreter. - */ -#if HAVE_FEXECVE - fexecve(fd, (char**)argv, envp_scrubbed); -#else - /* fallback: unfortunately must use the path */ - execve(resolved_path, (char**)argv, envp_scrubbed); -#endif - _exit(127); - } - - /* parent */ - close(fd); - - int status; - int ret = os_wait(pid, &status); - if (ret == -1) { - /* LCOV_EXCL_START */ - log_error(errno, "Wait %s failed. %s.", resolved_path, strerror(errno)); - return -1; - /* LCOV_EXCL_STOP */ - } - - if (WIFEXITED(status)) { - return WEXITSTATUS(status); - } else if (WIFSIGNALED(status)) { - return 128 + WTERMSIG(status); - } - - return -1; -} - -OS_FILE* os_popen(const char** argv) -{ - int stdout_fd = -1; - pid_t pid = os_spawn_stdout(argv, &stdout_fd); - if (pid < 0) { - return 0; - } - - FILE* fp = fdopen(stdout_fd, "r"); - if (!fp) { - /* LCOV_EXCL_START */ - int saved_errno = errno; - close(stdout_fd); - int status; - os_wait(pid, &status); - errno = saved_errno; - return 0; - /* LCOV_EXCL_STOP */ - } - - OS_FILE* os_file = malloc_nofail(sizeof(OS_FILE)); - - os_file->fp = fp; - os_file->pid = pid; - - return os_file; -} - -char* os_fgets(char* s, int size, OS_FILE* stream) -{ - return fgets(s, size, stream->fp); -} - -int os_pclose(OS_FILE* stream) -{ - /* leep handles locally to free container block immediately */ - FILE* fp = stream->fp; - pid_t pid = stream->pid; - free(stream); - - /* close buffered stream, releasing the underlying fd */ - fclose(fp); - - int status = 0; - if (os_wait(pid, &status) < 0) { - return -1; - } - - return status; -} - -/****************************************************************************/ -/* os */ - -uint64_t os_tick(void) -{ -#if HAVE_MACH_ABSOLUTE_TIME - /* for Mac OS X */ - return mach_absolute_time(); -#elif HAVE_CLOCK_GETTIME && (defined(CLOCK_MONOTONIC) || defined(CLOCK_MONOTONIC_RAW)) - /* for Linux */ - struct timespec tv; - - /* nanosecond precision with clock_gettime() */ -#if defined(CLOCK_MONOTONIC_RAW) - if (clock_gettime(CLOCK_MONOTONIC_RAW, &tv) != 0) { -#else - if (clock_gettime(CLOCK_MONOTONIC, &tv) != 0) { -#endif - /* LCOV_EXCL_START */ - return 0; - /* LCOV_EXCL_STOP */ - } - - return tv.tv_sec * 1000000000ULL + tv.tv_nsec; -#else - /* other platforms */ - struct timeval tv; - - /* microsecond precision with gettimeofday() */ - if (gettimeofday(&tv, 0) != 0) { - /* LCOV_EXCL_START */ - return 0; - /* LCOV_EXCL_STOP */ - } - - return tv.tv_sec * 1000000ULL + tv.tv_usec; -#endif -} - -uint64_t os_tick_ms(void) -{ - struct timeval tv; - - if (gettimeofday(&tv, 0) != 0) { - /* LCOV_EXCL_START */ - return 0; - /* LCOV_EXCL_STOP */ - } - - return tv.tv_sec * 1000ULL + tv.tv_usec / 1000; -} - -int os_randomize(void* ptr, size_t size) -{ - int f; - ssize_t ret; - - f = open("/dev/urandom", O_RDONLY); - if (f == -1) { - /* LCOV_EXCL_START */ - return -1; - /* LCOV_EXCL_STOP */ - } - - ret = read(f, ptr, size); - if (ret < 0 || (size_t)ret != size) { - /* LCOV_EXCL_START */ - close(f); - return -1; - /* LCOV_EXCL_STOP */ - } - - if (close(f) != 0) { - /* LCOV_EXCL_START */ - return -1; - /* LCOV_EXCL_STOP */ - } - - return 0; -} - -void os_clear(void) -{ - /* ANSI codes */ - printf("\033[H"); /* cursor at topleft */ - printf("\033[2J"); /* clear screen */ -} - -/* LCOV_EXCL_START */ -void os_abort(void) -{ -#if HAVE_BACKTRACE && HAVE_BACKTRACE_SYMBOLS - void* stack[32]; - char** messages; - unsigned size; - unsigned i; -#endif - - printf("Stacktrace of " PACKAGE " v" VERSION); -#ifdef _linux - printf(", linux"); -#endif -#ifdef __GNUC__ - printf(", gcc " __VERSION__); -#endif - printf(", %d-bit", (int)sizeof(void*) * 8); - printf(", PATH_MAX=%d", PATH_MAX); - printf("\n"); - -#if HAVE_BACKTRACE && HAVE_BACKTRACE_SYMBOLS - size = backtrace(stack, 32); - - messages = backtrace_symbols(stack, size); - - for (i = 1; i < size; ++i) { - const char* msg; - - if (messages) - msg = messages[i]; - else - msg = ""; - - printf("[bt] %02u: %s\n", i, msg); - - if (messages) { - int ret; - char addr2line[1024]; - size_t j = 0; - while (msg[j] != '(' && msg[j] != ' ' && msg[j] != 0) - ++j; - - snprintf(addr2line, sizeof(addr2line), "addr2line %p -e %.*s", stack[i], (unsigned)j, msg); - - ret = system(addr2line); - if (WIFEXITED(ret) && WEXITSTATUS(ret) != 0) - printf("exit:%d\n", WEXITSTATUS(ret)); - if (WIFSIGNALED(ret)) - printf("signal:%d\n", WTERMSIG(ret)); - } - } -#endif - - printf("Please report this error to the SnapRAID Issues:\n"); - printf("https://github.com/amadvance/snapraid/issues\n"); - - abort(); -} -/* LCOV_EXCL_STOP */ - -void os_init(int opt) -{ - (void)opt; -} - -void os_done(void) -{ -} - - -#endif - diff --git a/cmdline/unixapp.c b/cmdline/unixapp.c index b8b060bc..3630d0a5 100644 --- a/cmdline/unixapp.c +++ b/cmdline/unixapp.c @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2013 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #ifndef __MINGW32__ /* Only for Unix */ -#include "os.h" +#include "app.h" #include "support.h" /****************************************************************************/ @@ -23,7 +23,7 @@ int exit_sync_needed = 2; volatile int global_interrupt = 0; -int app_global_interrupt(void) +int os_signal_interrupt(void) { return global_interrupt; } @@ -1295,7 +1295,6 @@ static int devuuid_blkid(uint64_t device, char* uuid, size_t uuid_size) } #endif - /** * Get the LABEL using libblkid. * It uses a cache to work without root permission, resulting in LABEL @@ -1594,118 +1593,6 @@ int devuuid(uint64_t device_id, const char* device_path, char* uuid, size_t uuid /****************************************************************************/ /* fs */ -int filephy(const char* path, uint64_t size, uint64_t* physical) -{ -#if HAVE_LINUX_FIEMAP_H - /* - * In Linux get the real physical address of the file - * Note that FIEMAP doesn't require root permission - */ - int f; - struct fiemap* fiemap; - size_t fiemap_size; - unsigned int blknum; - - f = open(path, O_RDONLY); - if (f == -1) { - /* LCOV_EXCL_START */ - return -1; - /* LCOV_EXCL_STOP */ - } - - /* - * First try with FIEMAP - * if works for ext2, ext3, ext4, xfs, btrfs - */ - fiemap_size = sizeof(struct fiemap) + sizeof(struct fiemap_extent); - fiemap = malloc_nofail(fiemap_size); - memset(fiemap, 0, fiemap_size); - fiemap->fm_start = 0; - fiemap->fm_length = ~0ULL; - fiemap->fm_flags = FIEMAP_FLAG_SYNC; /* required to ensure that just created files report a valid address and not 0 */ - fiemap->fm_extent_count = 1; /* we are interested only at the first block */ - - if (ioctl(f, FS_IOC_FIEMAP, fiemap) != -1) { - uint32_t flags = fiemap->fm_extents[0].fe_flags; - uint64_t offset = fiemap->fm_extents[0].fe_physical; - - /* check some condition for validating the offset */ - if (flags & FIEMAP_EXTENT_DATA_INLINE) { - /* if the data is inline, we don't have an offset to report */ - *physical = FILEPHY_WITHOUT_OFFSET; - } else if (flags & FIEMAP_EXTENT_UNKNOWN) { - /* if the offset is unknown, we don't have an offset to report */ - *physical = FILEPHY_WITHOUT_OFFSET; - } else if (offset == 0) { - /* - * 0 is the general fallback for file-systems when - * they don't have an offset to report - */ - *physical = FILEPHY_WITHOUT_OFFSET; - } else { - /* finally report the real offset */ - *physical = offset + FILEPHY_REAL_OFFSET; - } - - free(fiemap); - - if (close(f) == -1) - return -1; - return 0; - } - - free(fiemap); - - /* if the file is empty, FIBMAP doesn't work, and we don't even try to use it */ - if (size == 0) { - *physical = FILEPHY_WITHOUT_OFFSET; - if (close(f) == -1) - return -1; - return 0; - } - - /* - * Then try with FIBMAP - * it works for jfs, reiserfs, ntfs-3g - * in exfat it always returns 0, that it's anyway better than the fake inodes - */ - blknum = 0; /* first block */ - if (ioctl(f, FIBMAP, &blknum) != -1) { - *physical = blknum + FILEPHY_REAL_OFFSET; - if (close(f) == -1) - return -1; - return 0; - } - - /* - * Otherwise don't use anything, and keep the directory traversal order - * at now this should happen only for vfat - * and it's surely better than using fake inodes - */ - *physical = FILEPHY_UNREPORTED_OFFSET; - if (close(f) == -1) - return -1; -#else - /* - * In a generic Unix use a dummy value for all the files - * We don't want to risk to use the inode without knowing - * if it really improves performance. - * In this way we keep them in the directory traversal order - * that at least keeps files in the same directory together. - * Note also that in newer file-system with snapshot, like ZFS, - * the inode doesn't represent even more the disk position, because files - * are not overwritten in place, but rewritten in another location - * of the disk. - */ - *physical = FILEPHY_UNREPORTED_OFFSET; - - (void)path; /* not used here */ - (void)size; -#endif - - return 0; -} - /* from man statfs */ #define ADFS_SUPER_MAGIC 0xadf5 #define AFFS_SUPER_MAGIC 0xADFF @@ -1800,6 +1687,9 @@ int filephy(const char* path, uint64_t size, uint64_t* physical) #define UFS_BYTESWAPPED_SUPER_MAGIC 0x54190100 #define VMHGFS_SUPER_MAGIC 0xBACBACBC #define VZFS_SUPER_MAGIC 0x565A4653 +#define BTRFS_SUPER_MAGIC 0x9123683E +#define BCACHEFS_SUPER_MAGIC 0xCA451A4E +#define ZFS_SUPER_MAGIC 0x2FC12FC1 struct filesystem_entry { unsigned id; @@ -2035,6 +1925,9 @@ int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_har return 0; } +/****************************************************************************/ +/* snapshot */ + #if HAVE_LINUX_DEVICE static int fssnapshot_inode(const char* path, uint32_t magic, uint64_t root_inode, struct fssnapshot_struct* fss) { diff --git a/cmdline/util.c b/cmdline/util.c index eda87eb6..021e2567 100644 --- a/cmdline/util.c +++ b/cmdline/util.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2011 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #include "support.h" #include "util.h" diff --git a/configure.ac b/configure.ac index 4807bb4f..9fea8247 100644 --- a/configure.ac +++ b/configure.ac @@ -7,7 +7,7 @@ AC_CANONICAL_HOST AC_CANONICAL_BUILD AC_CANONICAL_TARGET AM_INIT_AUTOMAKE([foreign no-dependencies subdir-objects]) -AC_CONFIG_SRCDIR([cmdline/portable.h]) +AC_CONFIG_SRCDIR([os/os.h]) AC_CONFIG_HEADERS([config.h]) dnl Enables as many system-specific extensions as reasonably possible @@ -72,7 +72,7 @@ AC_HEADER_DIRENT AC_HEADER_SYS_WAIT AC_CHECK_HEADERS([fcntl.h stddef.h stdint.h stdlib.h string.h limits.h time.h sys/time.h]) AC_CHECK_HEADERS([unistd.h getopt.h io.h inttypes.h byteswap.h]) -AC_CHECK_HEADERS([pthread.h math.h]) +AC_CHECK_HEADERS([pthread.h math.h syslog.h grp.h pwd.h]) AC_CHECK_HEADERS([sys/file.h sys/ioctl.h sys/sysmacros.h sys/mkdev.h sys/mount.h]) AC_CHECK_HEADERS([linux/fiemap.h linux/fs.h linux/btrfs.h execinfo.h]) AC_CHECK_HEADERS([mach/mach_time.h sys/sysctl.h]) diff --git a/cmdline/mingw.c b/os/mingw.c similarity index 63% rename from cmdline/mingw.c rename to os/mingw.c index 98a86f39..a8ff1fd6 100644 --- a/cmdline/mingw.c +++ b/os/mingw.c @@ -1,13 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2011 Andrea Mazzoleni +// Copyright (C) 2026 Andrea Mazzoleni -#include "portable.h" +#include "os/portable.h" #ifdef __MINGW32__ /* Only for MingW */ #include "os.h" -#include "app.h" /* for app_global_interrupt() */ -#include "support.h" + +#include "tommyds/tommylist.h" + +#include /* CreateEnvironmentBlock */ /****************************************************************************/ /* global */ @@ -89,71 +91,143 @@ void os_signal_init(void (*handler_term)(int sig), void (*handler_hup)(int sig)) } } +const char* os_signal_name(int sig) +{ + switch (sig) { +#ifdef SIGHUP + case SIGHUP : return "SIGHUP"; +#endif +#ifdef SIGINT + case SIGINT : return "SIGINT"; +#endif +#ifdef SIGQUIT + case SIGQUIT : return "SIGQUIT"; +#endif +#ifdef SIGILL + case SIGILL : return "SIGILL"; +#endif +#ifdef SIGTRAP + case SIGTRAP : return "SIGTRAP"; +#endif +#ifdef SIGABRT + case SIGABRT : return "SIGABRT"; +#endif +#ifdef SIGBUS + case SIGBUS : return "SIGBUS"; +#endif +#ifdef SIGFPE + case SIGFPE : return "SIGFPE"; +#endif +#ifdef SIGKILL + case SIGKILL : return "SIGKILL"; +#endif +#ifdef SIGUSR1 + case SIGUSR1 : return "SIGUSR1"; +#endif +#ifdef SIGSEGV + case SIGSEGV : return "SIGSEGV"; +#endif +#ifdef SIGUSR2 + case SIGUSR2 : return "SIGUSR2"; +#endif +#ifdef SIGPIPE + case SIGPIPE : return "SIGPIPE"; +#endif +#ifdef SIGALRM + case SIGALRM : return "SIGALRM"; +#endif +#ifdef SIGTERM + case SIGTERM : return "SIGTERM"; +#endif + } + + return "UNKNOWN"; +} + /****************************************************************************/ /* convert */ -wchar_t* u8tou16(wchar_t* conv_buf, const char* src) +wchar_t* u8tou16_mayfail(wchar_t* conv_buf, size_t number_of_wchar, const char* src, size_t number_of_char, size_t* result_length_without_terminator) +{ + /* MB_ERR_INVALID_CHARS forces the API to fail on malformed UTF-8 sequences */ + int ret = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, number_of_char, conv_buf, number_of_wchar); + if (ret <= 0) + return 0; + + if (result_length_without_terminator) + *result_length_without_terminator = ret; + + return conv_buf; +} + +char* u16tou8_mayfail(char* conv_buf, size_t number_of_char, const wchar_t* src, size_t number_of_wchar, size_t* result_length_without_terminator) +{ + /* WC_ERR_INVALID_CHARS forces the API to fail on malformed UTF-16 sequences */ + int ret = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, src, number_of_wchar, conv_buf, number_of_char, 0, 0); + if (ret <= 0) + return 0; + + if (result_length_without_terminator) + *result_length_without_terminator = ret; + + return conv_buf; +} + +static wchar_t* u8tou16_force(wchar_t* conv_buf, size_t number_of_wchar, const char* src, size_t number_of_char, size_t* result_length_without_terminator) { /* flags at 0 forces the API to never fail on malformed UTF-8 sequences */ - int ret = MultiByteToWideChar(CP_UTF8, 0, src, -1, conv_buf, CONV_MAX); + int ret = MultiByteToWideChar(CP_UTF8, 0, src, number_of_char, conv_buf, number_of_wchar); if (ret <= 0) { DWORD error = GetLastError(); if (error == ERROR_INSUFFICIENT_BUFFER) { - log_fatal(EINTERNAL, "Path too long converting '%s' from UTF-8 to UTF-16\n", src); + os_syslog(OS_LVL_CRITICAL, "path too long converting '%s' from UTF-8 to UTF-16", src); } else { - log_fatal(EINTERNAL, "Error %u converting '%s' from UTF-8 to UTF-16\n", (unsigned)error, src); + os_syslog(OS_LVL_CRITICAL, "error %u converting '%s' from UTF-8 to UTF-16", (unsigned)error, src); } os_abort(); } - return conv_buf; -} - -/** - * Convert a generic string from UTF16 to UTF8. - */ -static char* u16tou8ex_may_fail(char* conv_buf, const wchar_t* src, size_t number_of_wchar, size_t* result_length_without_terminator) -{ - /* WC_ERR_INVALID_CHARS forces the API to fail on malformed UTF-16 sequences */ - int ret = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, src, number_of_wchar, conv_buf, CONV_MAX, 0, 0); - if (ret <= 0) - return 0; - - *result_length_without_terminator = ret; + if (result_length_without_terminator) + *result_length_without_terminator = ret; return conv_buf; } -static char* u16tou8ex(char* conv_buf, const wchar_t* src, size_t number_of_wchar, size_t* result_length_without_terminator) +static char* u16tou8_force(char* conv_buf, size_t number_of_char, const wchar_t* src, size_t number_of_wchar, size_t* result_length_without_terminator) { /* flags at 0 forces the API to never fail on malformed UTF-16 sequences */ - int ret = WideCharToMultiByte(CP_UTF8, 0, src, number_of_wchar, conv_buf, CONV_MAX, 0, 0); + int ret = WideCharToMultiByte(CP_UTF8, 0, src, number_of_wchar, conv_buf, number_of_char, 0, 0); if (ret <= 0) { DWORD error = GetLastError(); if (error == ERROR_INSUFFICIENT_BUFFER) { - log_fatal(EINTERNAL, "Path too long converting from UTF-16 to UTF-8 with len %u\n", (unsigned)number_of_wchar); + os_syslog(OS_LVL_CRITICAL, "path too long converting from UTF-16 to UTF-8 with len %u", (unsigned)number_of_wchar); } else { - log_fatal(EINTERNAL, "Error %u converting from UTF-16 to UTF-8 with len %u\n", (unsigned)error, (unsigned)number_of_wchar); + os_syslog(OS_LVL_CRITICAL, "error %u converting from UTF-16 to UTF-8 with len %u", (unsigned)error, (unsigned)number_of_wchar); if (src != 0) { for (size_t i = 0; i < number_of_wchar; ++i) { - log_fatal(EINTERNAL, "%4u: %04x\n", (unsigned)i, src[i]); + os_syslog(OS_LVL_CRITICAL, "%4u: %04x", (unsigned)i, src[i]); } } } os_abort(); } - *result_length_without_terminator = ret; + if (result_length_without_terminator) + *result_length_without_terminator = ret; return conv_buf; } +wchar_t* u8tou16(wchar_t* conv_buf, const char* src) +{ + /* convert also the 0 terminator */ + return u8tou16_force(conv_buf, CONV_MAX, src, strlen(src) + 1, 0); +} + char* u16tou8(char* conv_buf, const wchar_t* src) { - size_t len; - /* convert also the 0 terminator */ - return u16tou8ex(conv_buf, src, wcslen(src) + 1, &len); + return u16tou8_force(conv_buf, CONV_MAX, src, wcslen(src) + 1, 0); } /** @@ -221,9 +295,9 @@ wchar_t* convert_arg(wchar_t* conv_buf, const char* src, int only_if_required) if (ret <= 0) { DWORD error = GetLastError(); if (error == ERROR_INSUFFICIENT_BUFFER) { - log_fatal(EINTERNAL, "Path too long converting '%s' from UTF-8 to UTF-16\n", src); + os_syslog(OS_LVL_CRITICAL, "oath too long converting '%s' from UTF-8 to UTF-16", src); } else { - log_fatal(EINTERNAL, "Error %u converting '%s' from UTF-8 to UTF-16\n", (unsigned)error, src); + os_syslog(OS_LVL_CRITICAL, "error %u converting '%s' from UTF-8 to UTF-16", (unsigned)error, src); } os_abort(); } @@ -278,13 +352,13 @@ static void windows_attr2stat(DWORD FileAttributes, DWORD ReparseTag, struct win if ((FileAttributes & FILE_ATTRIBUTE_DEVICE) != 0) { st->st_mode = S_IFBLK; st->st_desc = "device"; - } else if ((FileAttributes & FILE_ATTRIBUTE_OFFLINE) != 0) { /* Offline */ + } else if ((FileAttributes & FILE_ATTRIBUTE_OFFLINE) != 0) { /* Offline */ st->st_mode = S_IFCHR; st->st_desc = "offline"; - } else if ((FileAttributes & FILE_ATTRIBUTE_TEMPORARY) != 0) { /* Temporary */ + } else if ((FileAttributes & FILE_ATTRIBUTE_TEMPORARY) != 0) { /* Temporary */ st->st_mode = S_IFCHR; st->st_desc = "temporary"; - } else if ((FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { /* Reparse point */ + } else if ((FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { /* Reparse point */ switch (ReparseTag) { /* if we don't have the ReparseTag information */ case 0 : @@ -325,7 +399,7 @@ static void windows_attr2stat(DWORD FileAttributes, DWORD ReparseTag, struct win st->st_desc = "reparse-point"; break; } - } else if ((FileAttributes & FILE_ATTRIBUTE_SYSTEM) != 0) { /* System */ + } else if ((FileAttributes & FILE_ATTRIBUTE_SYSTEM) != 0) { /* System */ if ((FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { st->st_mode = S_IFCHR; st->st_desc = "system-directory"; @@ -397,7 +471,7 @@ static int windows_info2stat(const BY_HANDLE_FILE_INFORMATION* info, const FILE_ */ if (st->st_ino == (uint64_t)FILE_INVALID_FILE_ID) { errno = EINVAL; - log_fatal(errno, "Invalid inode number! Is this ReFS?\n"); + os_syslog(OS_LVL_INFO, "invalid inode number! Is this ReFS?"); return -1; } @@ -444,7 +518,7 @@ static int windows_stream2stat(const BY_HANDLE_FILE_INFORMATION* info, const FIL /* in ReFS the IDs are 128 bit, and the 64 bit interface may fail */ if (st->st_ino == (uint64_t)FILE_INVALID_FILE_ID) { errno = EINVAL; - log_fatal(errno, "Invalid inode number! Is this ReFS?\n"); + os_syslog(OS_LVL_INFO, "Invalid inode number! Is this ReFS?"); return -1; } @@ -497,11 +571,11 @@ static void windows_finddata2dirent(const WIN32_FIND_DATAW* info, struct windows const char* name; size_t len; - name = u16tou8ex(conv_buf, info->cFileName, wcslen(info->cFileName), &len); + name = u16tou8_force(conv_buf, CONV_MAX, info->cFileName, wcslen(info->cFileName), &len); if (len + 1 >= sizeof(dirent->d_name)) { - log_fatal(EINTERNAL, "Name too long\n"); - exit(EXIT_FAILURE); + os_syslog(OS_LVL_CRITICAL, "name too long"); + os_exit(); } memcpy(dirent->d_name, name, len); @@ -516,11 +590,11 @@ static int windows_stream2dirent(const BY_HANDLE_FILE_INFORMATION* info, const F const char* name; size_t len; - name = u16tou8ex(conv_buf, stream->FileName, stream->FileNameLength / 2, &len); + name = u16tou8_force(conv_buf, CONV_MAX, stream->FileName, stream->FileNameLength / 2, &len); if (len + 1 >= sizeof(dirent->d_name)) { - log_fatal(EINTERNAL, "Name too long\n"); - exit(EXIT_FAILURE); + os_syslog(OS_LVL_CRITICAL, "name too long"); + os_exit(); } memcpy(dirent->d_name, name, len); @@ -529,6 +603,9 @@ static int windows_stream2dirent(const BY_HANDLE_FILE_INFORMATION* info, const F return windows_stream2stat(info, stream, &dirent->d_stat); } +/** + * Convert Windows error to errno. + */ void windows_errno(DWORD error) { switch (error) { @@ -540,20 +617,20 @@ void windows_errno(DWORD error) */ errno = EINVAL; break; - case ERROR_HANDLE_EOF : /* in ReadFile() over the end of the file */ + case ERROR_HANDLE_EOF : /* in ReadFile() over the end of the file */ errno = EINVAL; break; case ERROR_FILE_NOT_FOUND : - case ERROR_PATH_NOT_FOUND : /* in GetFileAttributeW() if internal path not found */ + case ERROR_PATH_NOT_FOUND : /* in GetFileAttributeW() if internal path not found */ errno = ENOENT; break; - case ERROR_ACCESS_DENIED : /* in CreateDirectoryW() if dir is scheduled for deletion */ - case ERROR_CURRENT_DIRECTORY : /* in RemoveDirectoryW() if removing the current directory */ - case ERROR_SHARING_VIOLATION : /* in RemoveDirectoryW() if in use */ - case ERROR_WRITE_PROTECT : /* when dealing with read-only media/snapshot and trying to write to them */ + case ERROR_ACCESS_DENIED : /* in CreateDirectoryW() if dir is scheduled for deletion */ + case ERROR_CURRENT_DIRECTORY : /* in RemoveDirectoryW() if removing the current directory */ + case ERROR_SHARING_VIOLATION : /* in RemoveDirectoryW() if in use */ + case ERROR_WRITE_PROTECT : /* when dealing with read-only media/snapshot and trying to write to them */ errno = EACCES; break; - case ERROR_ALREADY_EXISTS : /* in CreateDirectoryW() if already exists */ + case ERROR_ALREADY_EXISTS : /* in CreateDirectoryW() if already exists */ errno = EEXIST; break; case ERROR_DISK_FULL : @@ -565,19 +642,22 @@ void windows_errno(DWORD error) case ERROR_NOT_ENOUGH_MEMORY : errno = ENOMEM; break; - case ERROR_NOT_SUPPORTED : /* in CreateSymlinkW() if not present in kernel32 */ + case ERROR_NOT_SUPPORTED : /* in CreateSymlinkW() if not present in kernel32 */ errno = ENOSYS; break; - case ERROR_PRIVILEGE_NOT_HELD : /* in CreateSymlinkW() if no SeCreateSymbolicLinkPrivilige permission */ + case ERROR_PRIVILEGE_NOT_HELD : /* in CreateSymlinkW() if no SeCreateSymbolicLinkPrivilige permission */ errno = EPERM; break; - case ERROR_IO_DEVICE : /* in ReadFile() and WriteFile() */ - case ERROR_CRC : /* in ReadFile() */ + case ERROR_IO_DEVICE : /* in ReadFile() and WriteFile() */ + case ERROR_CRC : /* in ReadFile() */ errno = EIO; break; + case WSAEADDRINUSE : + errno = EADDRINUSE; + break; default : errno = ENXIO; - log_fatal(errno, "Unexpected Windows error %lu.\n", error); + os_syslog(OS_LVL_INFO, "unexpected Windows error %lu", error); break; } } @@ -684,115 +764,6 @@ int windows_rmdir(const char* file) return 0; } -static BOOL GetFilePhysicalOffset(HANDLE h, uint64_t* physical) -{ - STARTING_VCN_INPUT_BUFFER svib; - unsigned char rpb_buffer[sizeof(RETRIEVAL_POINTERS_BUFFER)]; - RETRIEVAL_POINTERS_BUFFER* rpb = (RETRIEVAL_POINTERS_BUFFER*)&rpb_buffer; - BOOL ret; - DWORD n; - - /* in Wine FSCTL_GET_RETRIEVAL_POINTERS is not supported */ - if (is_wine) { - *physical = FILEPHY_UNREPORTED_OFFSET; - return TRUE; - } - - /* set the output variable, just to be safe */ - rpb->ExtentCount = 0; - - /* read the physical address */ - svib.StartingVcn.QuadPart = 0; - ret = DeviceIoControl(h, FSCTL_GET_RETRIEVAL_POINTERS, &svib, sizeof(svib), rpb_buffer, sizeof(rpb_buffer), &n, 0); - if (!ret) { - DWORD error = GetLastError(); - if (error == ERROR_MORE_DATA) { - /* - * We ignore ERROR_MODE_DATA because we are interested only at the first entry - * and this is the expected error if the files has more entries - */ - } else if (error == ERROR_HANDLE_EOF) { - /* - * If the file is small, it can be stored in the Master File Table (MFT) - * and then it doesn't have a physical address - * In such case we report a specific fake address, to report this special condition - * that it's different from the 0 offset reported by the underline file system - */ - *physical = FILEPHY_WITHOUT_OFFSET; - return TRUE; - } else if (error == ERROR_NOT_SUPPORTED) { - /* for disks shared on network this operation is not supported */ - *physical = FILEPHY_UNREPORTED_OFFSET; - return TRUE; - } else { - return FALSE; - } - } - - if (rpb->ExtentCount < 1) - *physical = FILEPHY_UNREPORTED_OFFSET; - else - *physical = rpb->Extents[0].Lcn.QuadPart + FILEPHY_REAL_OFFSET; - - return TRUE; -} - -int lstat_sync(const char* file, struct windows_stat* st, uint64_t* physical) -{ - wchar_t conv_buf[CONV_MAX]; - BY_HANDLE_FILE_INFORMATION info; - FILE_ATTRIBUTE_TAG_INFO tag; - HANDLE h; - - /* - * Open the handle of the file. - * - * Use FILE_FLAG_BACKUP_SEMANTICS to open directories (it's just ignored for files). - * Use FILE_FLAG_OPEN_REPARSE_POINT to open symbolic links and not the their target. - * - * Note that even with FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - * and FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT some paths - * cannot be opened like "C:\System Volume Information" resulting - * in error ERROR_ACCESS_DENIED. - */ - h = CreateFileW(convert(conv_buf, file), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, 0); - if (h == INVALID_HANDLE_VALUE) { - windows_errno(GetLastError()); - return -1; - } - - if (!GetFileInformationByHandle(h, &info)) { - DWORD error = GetLastError(); - CloseHandle(h); - windows_errno(error); - return -1; - } - - if (!GetReparseTagInfoByHandle(h, &tag, info.dwFileAttributes)) { - DWORD error = GetLastError(); - CloseHandle(h); - windows_errno(error); - return -1; - } - - /* read the physical offset, only if a pointer is provided */ - if (physical != 0) { - if (!GetFilePhysicalOffset(h, physical)) { - DWORD error = GetLastError(); - CloseHandle(h); - windows_errno(error); - return -1; - } - } - - if (!CloseHandle(h)) { - windows_errno(GetLastError()); - return -1; - } - - return windows_info2stat(&info, &tag, st); -} - int windows_stat(const char* file, struct windows_stat* st) { wchar_t conv_buf[CONV_MAX]; @@ -920,8 +891,8 @@ int windows_fsync(int fd) * So, we use now an extended error reporting. */ windows_errno(error); - log_fatal(errno, "Unexpected Windows INVALID_HANDLE error in FlushFileBuffers().\n"); - log_fatal(errno, "Are you using ATA-over-Ethernet ? Please report it.\n"); + os_syslog(OS_LVL_INFO, "unexpected Windows INVALID_HANDLE error in FlushFileBuffers()"); + os_syslog(OS_LVL_INFO, "are you using ATA-over-Ethernet? Please report it."); return -1; case ERROR_ACCESS_DENIED : @@ -977,8 +948,8 @@ static int windows_vsync(const wchar_t* volume) * So, we use now an extended error reporting. */ windows_errno(error); - log_fatal(errno, "Unexpected Windows INVALID_HANDLE error in FlushFileBuffers().\n"); - log_fatal(errno, "Are you using ATA-over-Ethernet ? Please report it.\n"); + os_syslog(OS_LVL_INFO, "unexpected Windows INVALID_HANDLE error in FlushFileBuffers()"); + os_syslog(OS_LVL_INFO, "are you using ATA-over-Ethernet? Please report it."); return -1; case ERROR_ACCESS_DENIED : @@ -1337,8 +1308,8 @@ static windows_dir* windows_opendir_find(const char* dir) dirstream = malloc(sizeof(windows_dir)); if (!dirstream) { - log_fatal(EFAULT, "Low memory\n"); - exit(EXIT_FAILURE); + errno = ENOMEM; + return 0; } wdir = convert(conv_buf, dir); @@ -1467,25 +1438,23 @@ static windows_dir* windows_opendir_stream(const char* dir) { wchar_t conv_buf[CONV_MAX]; windows_dir* dirstream; - WCHAR* wdir; dirstream = malloc(sizeof(windows_dir)); if (!dirstream) { - log_fatal(EFAULT, "Low memory\n"); - exit(EXIT_FAILURE); + errno = ENOMEM; + return 0; } - wdir = convert(conv_buf, dir); - /* uses a 64 kB buffer for reading directory */ dirstream->buffer_size = 64 * 1024; dirstream->buffer = malloc(dirstream->buffer_size); if (!dirstream->buffer) { - log_fatal(EFAULT, "Low memory\n"); - exit(EXIT_FAILURE); + free(dirstream); + errno = ENOMEM; + return 0; } - dirstream->h = CreateFileW(wdir, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); + dirstream->h = CreateFileW(convert(conv_buf, dir), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); if (dirstream->h == INVALID_HANDLE_VALUE) { DWORD error = GetLastError(); free(dirstream->buffer); @@ -1593,7 +1562,7 @@ const char* windows_stat_desc(struct stat* st) unsigned windows_sleep(unsigned seconds) { while (seconds) { - if (app_global_interrupt()) /* SIGINT should stop the sleep */ + if (os_signal_interrupt()) /* SIGINT should stop the sleep */ break; Sleep(1000); --seconds; @@ -1744,7 +1713,7 @@ int windows_readlink(const char* file, char* buffer, size_t size) } /* convert the name to UTF-8 */ - name = u16tou8ex(conv_buf_name, + name = u16tou8_force(conv_buf_name, CONV_MAX, rdb->SymbolicLinkReparseBuffer.PathBuffer + rdb->SymbolicLinkReparseBuffer.PrintNameOffset, rdb->SymbolicLinkReparseBuffer.PrintNameLength / 2, &len); @@ -1764,137 +1733,6 @@ int windows_readlink(const char* file, char* buffer, size_t size) return len; } -int filephy(const char* file, uint64_t size, uint64_t* physical) -{ - wchar_t conv_buf[CONV_MAX]; - HANDLE h; - - (void)size; - - /* open the handle of the file */ - h = CreateFileW(convert(conv_buf, file), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, 0, 0); - if (h == INVALID_HANDLE_VALUE) { - windows_errno(GetLastError()); - return -1; - } - - if (!GetFilePhysicalOffset(h, physical)) { - DWORD error = GetLastError(); - CloseHandle(h); - windows_errno(error); - return -1; - } - - CloseHandle(h); - return 0; -} - -int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_hardlinks, uint64_t* total_space, uint64_t* free_space, char* fstype, size_t fstype_size, char* fslabel, size_t fslabel_size) -{ - wchar_t conv_buf[CONV_MAX]; - - /* all FAT/exFAT/NTFS when managed from Windows have persistent inodes */ - if (has_persistent_inode) - *has_persistent_inode = 1; - - /* NTFS doesn't synchronize hardlinks metadata */ - if (has_syncronized_hardlinks) - *has_syncronized_hardlinks = 0; - - if (free_space || total_space) { - ULARGE_INTEGER total_bytes; - ULARGE_INTEGER total_free_bytes; - DWORD attr; - char dir[PATH_MAX]; - - if (strlen(path) + 1 > sizeof(dir)) { - windows_errno(ERROR_BUFFER_OVERFLOW); - return -1; - } - - strcpy(dir, path); - - /* get the file attributes */ - attr = GetFileAttributesW(convert(conv_buf, dir)); - if (attr == INVALID_FILE_ATTRIBUTES) { - DWORD error = GetLastError(); - - if (error != ERROR_FILE_NOT_FOUND) { - windows_errno(error); - return -1; - } - - /* - * If it doesn't exist, we assume a file - * and we check for the containing dir - */ - attr = 0; - } - - /* if it's not a directory, truncate the file name */ - if ((attr & FILE_ATTRIBUTE_DIRECTORY) == 0) { - char* slash = strrchr(dir, '/'); - - /** - * Cut the file name, but leave the last slash. - * - * This is done because a MSDN comment about using of UNC paths. - * - * MSDN 'GetDiskFreeSpaceEx function' - * http://msdn.microsoft.com/en-us/library/windows/desktop/aa364937%28v=vs.85%29.aspx - * If this parameter is a UNC name, it must include a trailing backslash, - * for example, "\\MyServer\MyShare\". - */ - if (slash) - slash[1] = 0; - } - - /* - * Get the free space of the directory - * note that it must be a directory - */ - if (!GetDiskFreeSpaceExW(convert(conv_buf, dir), 0, &total_bytes, &total_free_bytes)) { - windows_errno(GetLastError()); - return -1; - } - - if (total_space) - *total_space = total_bytes.QuadPart; - if (free_space) - *free_space = total_free_bytes.QuadPart; - } - - if (fstype && fslabel) { - wchar_t volume_root[PATH_MAX]; - wchar_t fs_name[PATH_MAX]; - wchar_t vol_name[PATH_MAX]; - - fstype[0] = 0; - fslabel[0] = 0; - if (GetVolumePathNameW(convert(conv_buf, path), volume_root, PATH_MAX)) { - if (GetVolumeInformationW(volume_root, vol_name, PATH_MAX, 0, 0, 0, fs_name, PATH_MAX)) { - char u8[CONV_MAX]; - size_t len; - char* ret; - - ret = u16tou8ex_may_fail(u8, fs_name, wcslen(fs_name), &len); - if (ret != 0 && len + 1 <= fstype_size) { - memcpy(fstype, u8, len); - fstype[len] = 0; - } - - ret = u16tou8ex_may_fail(u8, vol_name, wcslen(vol_name), &len); - if (ret != 0 && len + 1 <= fslabel_size) { - memcpy(fslabel, u8, len); - fslabel[len] = 0; - } - } - } - } - - return 0; -} - /* ensure to call the real C strerror() */ #undef strerror @@ -1928,6 +1766,9 @@ const char* windows_strerror(int err) return error; } +/* restore the define used later */ +#define strerror windows_strerror + ssize_t windows_read(int fd, void* buffer, size_t size) { HANDLE h; @@ -2052,7 +1893,7 @@ retry: * https://sourceforge.net/p/snapraid/discussion/1677233/thread/a7c25ba9/ */ if (err == ERROR_NO_SYSTEM_RESOURCES) { - log_fatal(EEXTERNAL, "Unexpected Windows ERROR_NO_SYSTEM_RESOURCES in pread(), retrying...\n"); + os_syslog(OS_LVL_ERROR, "unexpected Windows ERROR_NO_SYSTEM_RESOURCES in pread(), retrying..."); Sleep(50); goto retry; } @@ -2090,7 +1931,7 @@ retry: DWORD err = GetLastError(); /* See windows_pread() for comments on this error management */ if (err == ERROR_NO_SYSTEM_RESOURCES) { - log_fatal(EEXTERNAL, "Unexpected Windows ERROR_NO_SYSTEM_RESOURCES in pwrite(), retrying...\n"); + os_syslog(OS_LVL_ERROR, "unexpected Windows ERROR_NO_SYSTEM_RESOURCES in pwrite(), retrying..."); Sleep(50); goto retry; } @@ -2102,486 +1943,189 @@ retry: return count; } -size_t windows_direct_size(void) +struct tm* windows_gmtime_r(const time_t* timer, struct tm* result) { - SYSTEM_INFO si; - - GetSystemInfo(&si); - - /* - * MSDN 'File Buffering' - * https://msdn.microsoft.com/en-us/library/windows/desktop/cc644950%28v=vs.85%29.aspx - * - * "Therefore, in most situations, page-aligned memory will also be sector-aligned," - * "because the case where the sector size is larger than the page size is rare." - */ - return si.dwPageSize; + /* Windows returns 0 on success, unlike POSIX which returns the pointer */ + if (gmtime_s(result, timer) == 0) { + return result; + } + return NULL; } -#define WINDOWS_NTFS_MAGIC 0x5346544E /* 'N','T','F','S' */ - -#define PS_CMD_MAX 4096 - -#define SNAPSHOT_GUID ".guid" - -/* - * PowerShell helper - * - * Runs a PowerShell one-liner via _popen() and captures the first line - * of stdout into `out` as UTF-8. - * `out` may be NULL when output is not needed. - * - * The caller builds `command` as a string using snprintf(). - * Any embedded path literals must use single quotes so they survive - * the outer double-quote wrapping passed to cmd.exe. - * - * Returns 0 on success, -1 on failure. - */ -static int windows_ps(const char* ps_command, char* out, size_t out_size) +struct tm* windows_localtime_r(const time_t* timer, struct tm* result) { - char cmd[PS_CMD_MAX]; - FILE* fp; - int ret; - - ret = snprintf(cmd, sizeof(cmd), "powershell.exe -NoProfile -NonInteractive -Command \"%s\" 2>nul", ps_command); - if (ret < 0 || ret >= (int)sizeof(cmd)) { - errno = EINVAL; - return -1; + /* Windows returns 0 on success, unlike POSIX which returns the pointer */ + if (localtime_s(result, timer) == 0) { + return result; } + return NULL; +} - fp = _popen(cmd, "r"); - if (!fp) { +char* windows_realpath(const char* path, char* resolved_path) +{ + wchar_t conv_buf[CONV_MAX]; + HANDLE h = CreateFileW(convert(conv_buf, path), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (h == INVALID_HANDLE_VALUE) { windows_errno(GetLastError()); - log_error(errno, "Failed to run PowerShell command '%s' (from popen).\n", cmd); - return -1; + return 0; } - if (out && out_size > 0) { - /* get the first line */ - if (!fgets(out, (int)out_size, fp)) { - out[0] = 0; + DWORD result = GetFinalPathNameByHandleW(h, conv_buf, CONV_MAX, FILE_NAME_NORMALIZED); + + CloseHandle(h); + + if (result == 0 || result >= CONV_MAX) { + windows_errno(GetLastError()); + return 0; + } + + resolved_path = u16tou8(resolved_path, conv_buf); + + /* Windows prefixes paths with "\\?\" (UNC). Skip it if you want a standard path */ + if (strncmp(resolved_path, "\\\\?\\", 4) == 0) { + memmove(resolved_path, resolved_path + 4, strlen(resolved_path) - 3); + } + + return resolved_path; +} + +/****************************************************************************/ +/* fs */ + +static BOOL GetFilePhysicalOffset(HANDLE h, uint64_t* physical) +{ + STARTING_VCN_INPUT_BUFFER svib; + unsigned char rpb_buffer[sizeof(RETRIEVAL_POINTERS_BUFFER)]; + RETRIEVAL_POINTERS_BUFFER* rpb = (RETRIEVAL_POINTERS_BUFFER*)&rpb_buffer; + BOOL ret; + DWORD n; + + /* in Wine FSCTL_GET_RETRIEVAL_POINTERS is not supported */ + if (is_wine) { + *physical = FILEPHY_UNREPORTED_OFFSET; + return TRUE; + } + + /* set the output variable, just to be safe */ + rpb->ExtentCount = 0; + + /* read the physical address */ + svib.StartingVcn.QuadPart = 0; + ret = DeviceIoControl(h, FSCTL_GET_RETRIEVAL_POINTERS, &svib, sizeof(svib), rpb_buffer, sizeof(rpb_buffer), &n, 0); + if (!ret) { + DWORD error = GetLastError(); + if (error == ERROR_MORE_DATA) { + /* + * We ignore ERROR_MODE_DATA because we are interested only at the first entry + * and this is the expected error if the files has more entries + */ + } else if (error == ERROR_HANDLE_EOF) { + /* + * If the file is small, it can be stored in the Master File Table (MFT) + * and then it doesn't have a physical address + * In such case we report a specific fake address, to report this special condition + * that it's different from the 0 offset reported by the underline file system + */ + *physical = FILEPHY_WITHOUT_OFFSET; + return TRUE; + } else if (error == ERROR_NOT_SUPPORTED) { + /* for disks shared on network this operation is not supported */ + *physical = FILEPHY_UNREPORTED_OFFSET; + return TRUE; } else { - /* trim spaces and newlines */ - strtrim(out); + return FALSE; } } - ret = _pclose(fp); - if (ret == -1) { - errno = EINVAL; - log_error(errno, "Failed to run PowerShell command '%s' (from pclose).\n", cmd); - return -1; - } - if (ret != 0) { - errno = EINVAL; - log_error(errno, "PowerShell command '%s' failed with '%d' (from pclose).\n", cmd, ret); - return -1; - } - - return 0; -} - -/** - * Validates a GUID string of the form: - * xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - * Does NOT accept braced forms like {xxxxxxxx-...}. - * - * Returns true if valid, false otherwise. - */ -int windows_guid_is_valid(const char* guid, int len) -{ - if (!guid || len != 38) - return -1; - - if (guid[0] != '{' || guid[37] != '}') - return -1; - - const char* inner = guid + 1; - - for (int i = 0; i < 36; ++i) { - if (i == 8 || i == 13 || i == 18 || i == 23) { - if (inner[i] != '-') - return -1; - } else { - char c = inner[i]; - int is_digit = (c >= '0' && c <= '9'); - int is_upper_hex = (c >= 'A' && c <= 'F'); - int is_lower_hex = (c >= 'a' && c <= 'f'); - - if (!is_digit && !is_upper_hex && !is_lower_hex) - return -1; - } - } - - return 0; -} - -static int windows_read_guid(const char* guid_link, char* guid, size_t guid_size) -{ - int len = windows_readlink(guid_link, guid, guid_size); - if (len < 0 && errno == ENOENT) { - guid[0] = 0; - return 0; - } - if (len < 0) { - log_error(errno, "Error readlink '%s'. %s.\n", guid_link, strerror(errno)); - return -1; - } - - if ((size_t)len >= guid_size) { - errno = ENAMETOOLONG; - log_error(errno, "GUID too long in '%s'.\n", guid_link); - return -1; - } - - guid[len] = 0; - - /* validate that the GUID contains only safe characters: [A-Za-z0-9-{}] */ - if (windows_guid_is_valid(guid, len) != 0) - return -1; - - return 0; -} - -static int windows_rebuild_link(const struct fssnapshot_struct* fss, const char* name) -{ - char cmd[PS_CMD_MAX]; - char device_path[PATH_MAX]; - char target_path[PATH_MAX]; - char link_path[PATH_MAX]; - char guid_link[PATH_MAX]; - char guid[PATH_MAX]; - - pathcpy(guid_link, sizeof(guid_link), fss->snapshot_dir); - pathcat(guid_link, sizeof(guid_link), name); - pathcat(guid_link, sizeof(guid_link), SNAPSHOT_GUID); - - pathcpy(link_path, sizeof(link_path), fss->snapshot_dir); - pathcat(link_path, sizeof(link_path), name); - - int ret = windows_read_guid(guid_link, guid, sizeof(guid)); - if (ret != 0) { - return -1; - } - - /* if no GUID, assume it absent */ - if (guid[0] == 0) { - /* remove stale links */ - windows_rmdir(link_path); - return 0; - } - - snprintf(cmd, sizeof(cmd), - "(Get-WmiObject Win32_ShadowCopy | Where-Object {$_.ID -eq '%s'}).DeviceObject", - guid); - - if (windows_ps(cmd, device_path, sizeof(device_path)) != 0) { - log_error(errno, "Error getting DeviceObject from GUID '%s'. %s.\n", guid, strerror(errno)); - return -1; - } - - /* if no device path, assume snapshot absent */ - if (device_path[0] == 0) { - /* remove stale links */ - windows_rmdir(guid_link); - windows_rmdir(link_path); - return 0; - } - - pathimport(target_path, sizeof(target_path), device_path); - pathslash(target_path, sizeof(target_path)); - - /* remove stale link if present */ - windows_rmdir(link_path); - - if (windows_symlink_directory(target_path, link_path) != 0) { - log_error(errno, "Error creating symlink '%s'. %s.\n", link_path, strerror(errno)); - return -1; - } - - return 0; -} - -static int windows_delete_link(const struct fssnapshot_struct* fss, const char* name) -{ - char link_path[PATH_MAX]; - - pathcpy(link_path, sizeof(link_path), fss->snapshot_dir); - pathcat(link_path, sizeof(link_path), name); - - /* remove link if present */ - windows_rmdir(link_path); - - return 0; -} - -int fssnapshot_mount(const char* dir, struct fssnapshot_struct* fss) -{ - wchar_t conv_buf_vol[CONV_MAX]; - char conv_buf_root[CONV_MAX]; - wchar_t volume_root[PATH_MAX]; - wchar_t volume_name[PATH_MAX]; - wchar_t fs_name[32]; - uint32_t magic; - - /* - * GetVolumePathNameW() accepts any path: file, directory, or deep - * subdirectory, and resolves it to the volume root (e.g. L"C:\"). - * The result always carries a trailing backslash. - * - * Use convert_if_required() to avoid the automatic addition of \\?\ - * made by convert() that is propagated in the resulting volume_root - */ - if (!GetVolumePathNameW(convert_if_required(conv_buf_vol, dir), volume_root, PATH_MAX)) { - windows_errno(GetLastError()); - log_error(errno, "Error getting VolumeRoot from '%s'. %s.\n", dir, strerror(errno)); - return -1; - } - - if (!GetVolumeInformationW(volume_root, 0, 0, 0, 0, 0, fs_name, 32)) { - windows_errno(GetLastError()); - log_error(errno, "Error getting information of VolumeRoot '%s'. %s.\n", u16tou8(conv_buf_root, volume_root), strerror(errno)); - return -1; - } - if (wcscmp(fs_name, L"NTFS") == 0) - magic = WINDOWS_NTFS_MAGIC; + if (rpb->ExtentCount < 1) + *physical = FILEPHY_UNREPORTED_OFFSET; else - return -1; /* support only NTFS */ + *physical = rpb->Extents[0].Lcn.QuadPart + FILEPHY_REAL_OFFSET; + + return TRUE; +} + +int lstat_sync(const char* file, struct windows_stat* st, uint64_t* physical) +{ + wchar_t conv_buf[CONV_MAX]; + BY_HANDLE_FILE_INFORMATION info; + FILE_ATTRIBUTE_TAG_INFO tag; + HANDLE h; /* - * Obtain the canonical volume name: \\?\Volume{GUID}\ + * Open the handle of the file. * - * GetVolumePathNameW() may return a drive letter path ("C:\") or a - * directory mount point ("D:\Mount\"). Neither is guaranteed to be - * accepted by Win32_ShadowCopy.Create() on volumes without a drive - * letter. GetVolumeNameForVolumeMountPointW() always returns the - * stable kernel-form GUID path that VSS requires, regardless of how - * many (or how few) mount points the volume has. + * Use FILE_FLAG_BACKUP_SEMANTICS to open directories (it's just ignored for files). + * Use FILE_FLAG_OPEN_REPARSE_POINT to open symbolic links and not the their target. * - * We store it in fss->dataset. + * Note that even with FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + * and FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT some paths + * cannot be opened like "C:\System Volume Information" resulting + * in error ERROR_ACCESS_DENIED. */ - if (!GetVolumeNameForVolumeMountPointW(volume_root, volume_name, PATH_MAX)) { + h = CreateFileW(convert(conv_buf, file), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, 0); + if (h == INVALID_HANDLE_VALUE) { windows_errno(GetLastError()); - log_error(errno, "Error getting VolumeName of VolumeRoot '%s'. %s.\n", u16tou8(conv_buf_root, volume_root), strerror(errno)); return -1; } - /* don't use pathimport for dataset to keep backslashes */ - pathcpy(fss->dataset, sizeof(fss->dataset), u16tou8(conv_buf_root, volume_name)); - - /* use pathimport to convert backslashes to slashes */ - pathimport(fss->root_dir, sizeof(fss->root_dir), u16tou8(conv_buf_root, volume_root)); - - /* the returned root_dir should match the start of the passed dir */ - if (pathncmp(fss->root_dir, dir, strlen(fss->root_dir)) != 0) { - errno = EINVAL; - log_error(errno, "Not matching VolumeRoot '%s' for '%s'.\n", fss->root_dir, dir); + if (!GetFileInformationByHandle(h, &info)) { + DWORD error = GetLastError(); + CloseHandle(h); + windows_errno(error); return -1; } - pathcpy(fss->snapshot_dir, sizeof(fss->snapshot_dir), fss->root_dir); - pathcat(fss->snapshot_dir, sizeof(fss->snapshot_dir), SNAPSHOT_CONTAINER "/"); - - if (windows_mkdir(fss->snapshot_dir) != 0 && errno != EEXIST) { - log_error(errno, "Error creating directory '%s'. %s.\n", fss->snapshot_dir, strerror(errno)); + if (!GetReparseTagInfoByHandle(h, &tag, info.dwFileAttributes)) { + DWORD error = GetLastError(); + CloseHandle(h); + windows_errno(error); return -1; } - /* refresh the links, after reboots they gets invalidated */ - if (windows_rebuild_link(fss, SNAPSHOT_PENDING) != 0) { + /* read the physical offset, only if a pointer is provided */ + if (physical != 0) { + if (!GetFilePhysicalOffset(h, physical)) { + DWORD error = GetLastError(); + CloseHandle(h); + windows_errno(error); + return -1; + } + } + + if (!CloseHandle(h)) { + windows_errno(GetLastError()); return -1; } - if (windows_rebuild_link(fss, SNAPSHOT_STABLE) != 0) { - return -1; - } - - fss->magic = magic; - - return 0; + return windows_info2stat(&info, &tag, st); } -int fssnapshot_stat(struct fssnapshot_struct* fss, const char* name, struct stat* st) +int filephy(const char* file, uint64_t size, uint64_t* physical) { - char link_path[PATH_MAX]; + wchar_t conv_buf[CONV_MAX]; + HANDLE h; - pathcpy(link_path, sizeof(link_path), fss->snapshot_dir); - pathcat(link_path, sizeof(link_path), name); - pathcat(link_path, sizeof(link_path), SNAPSHOT_GUID); /* we check the GUID as it's the one that matter */ + (void)size; - /* use lstat because we want to check the link */ - if (windows_lstat(link_path, st) != 0) + /* open the handle of the file */ + h = CreateFileW(convert(conv_buf, file), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, 0, 0); + if (h == INVALID_HANDLE_VALUE) { + windows_errno(GetLastError()); return -1; + } - /* it must be a link to a directory */ - if (!S_ISLNKDIR(st->st_mode)) + if (!GetFilePhysicalOffset(h, physical)) { + DWORD error = GetLastError(); + CloseHandle(h); + windows_errno(error); return -1; + } + CloseHandle(h); return 0; } -int fssnapshot_create(const struct fssnapshot_struct* fss, const char* name) -{ - char cmd[PS_CMD_MAX]; - char device_path[PATH_MAX]; - char target_path[PATH_MAX]; - char link_path[PATH_MAX]; - char guid_link[PATH_MAX]; - char out[PATH_MAX]; - - /* create VSS shadow copy and capture the system-assigned GUID */ - snprintf(cmd, sizeof(cmd), - "$r = (Get-WmiObject -List Win32_ShadowCopy).Create('%s', 'ClientAccessible'); " - "if ($r.ReturnValue -eq 0) { " - "Write-Output ('ID_' + $r.ShadowID); " - "} else { " - "Write-Output ('0x{0:X8}' -f $r.ReturnValue); " - "}", - fss->dataset); - if (windows_ps(cmd, out, sizeof(out)) != 0) { - log_error(errno, "Error creating snapshot of VolumeName '%s'. %s.\n", fss->dataset, strerror(errno)); - return -1; - } - if (strncmp(out, "ID_", 3) != 0) { - errno = ENODATA; - log_error(errno, "VSS snapshot creation of '%s' failed with error %s.\n", fss->dataset, out); - return -1; - } - - const char* guid = out + 3; - - /* resolve the shadow copy's device object path */ - snprintf(cmd, sizeof(cmd), "(Get-WmiObject Win32_ShadowCopy | Where-Object {$_.ID -eq '%s'}).DeviceObject", guid); - if (windows_ps(cmd, device_path, sizeof(device_path)) != 0) { - log_error(errno, "Error getting the snapshot DeviceObject from GUID '%s'. %s.\n", guid, strerror(errno)); - goto bail_and_delete; - } - if (device_path[0] == 0) { - errno = ENODATA; - log_error(errno, "Empty snapshot DeviceObject of GUID '%s'. %s.\n", guid, strerror(errno)); - goto bail_and_delete; - } - - /* use pathimport to convert backslashes to slashes */ - pathimport(target_path, sizeof(target_path), device_path); - - /* the ending slash is required, otherwise the link won't work */ - pathslash(target_path, sizeof(target_path)); - - pathcpy(link_path, sizeof(link_path), fss->snapshot_dir); - pathcat(link_path, sizeof(link_path), name); - - /* remove potential stale link */ - windows_rmdir(link_path); - - int ret = windows_symlink_directory(target_path, link_path); - if (ret != 0) { - log_error(errno, "Error creating symlink '%s'. %s.\n", link_path, strerror(errno)); - goto bail_and_delete; - } - - pathcpy(guid_link, sizeof(guid_link), fss->snapshot_dir); - pathcat(guid_link, sizeof(guid_link), name); - pathcat(guid_link, sizeof(guid_link), SNAPSHOT_GUID); - - /* store GUID persistently as symlink target */ - ret = windows_symlink_directory(guid, guid_link); - if (ret != 0) { - log_error(errno, "Error creating GUID symlink '%s'. %s.\n", guid_link, strerror(errno)); - windows_rmdir(link_path); - goto bail_and_delete; - } - - return 0; - -bail_and_delete: - /* destroy the shadow copy we just created */ - snprintf(cmd, sizeof(cmd), "Get-WmiObject Win32_ShadowCopy | Where-Object {$_.ID -eq '%s'} | Remove-WmiObject", guid); - windows_ps(cmd, 0, 0); - return -1; -} - -int fssnapshot_delete(const struct fssnapshot_struct* fss, const char* name) -{ - char guid[PATH_MAX]; - char cmd[PS_CMD_MAX]; - char guid_link[PATH_MAX]; - - pathcpy(guid_link, sizeof(guid_link), fss->snapshot_dir); - pathcat(guid_link, sizeof(guid_link), name); - pathcat(guid_link, sizeof(guid_link), SNAPSHOT_GUID); - - if (windows_read_guid(guid_link, guid, sizeof(guid)) != 0) - return -1; - - /* if the GUID link is gone, assume everything is gone */ - if (guid[0] == 0) { - /* remove as not valid anymore */ - windows_delete_link(fss, name); - return 0; - } - - /* destroy the VSS shadow copy */ - snprintf(cmd, sizeof(cmd), "Get-WmiObject Win32_ShadowCopy | Where-Object {$_.ID -eq '%s'} | Remove-WmiObject", guid); - int ret = windows_ps(cmd, 0, 0); - if (ret != 0) { - log_error(errno, "Error destroy the snapshot from GUID '%s'. %s.\n", guid, strerror(errno)); - /* remove as not valid anymore */ - windows_delete_link(fss, name); - return -1; - } - - /* remove live symlink if present */ - windows_delete_link(fss, name); - - /* remove persistent GUID symlink */ - if (windows_rmdir(guid_link) != 0) { - log_error(errno, "Error rmdir '%s'. %s.\n", guid_link, strerror(errno)); - return -1; - } - - return 0; -} - -int fssnapshot_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name) -{ - char old_guid[PATH_MAX]; - char new_guid[PATH_MAX]; - - pathcpy(old_guid, sizeof(old_guid), fss->snapshot_dir); - pathcat(old_guid, sizeof(old_guid), old_name); - pathcat(old_guid, sizeof(old_guid), SNAPSHOT_GUID); - pathcpy(new_guid, sizeof(new_guid), fss->snapshot_dir); - pathcat(new_guid, sizeof(new_guid), new_name); - pathcat(new_guid, sizeof(new_guid), SNAPSHOT_GUID); - - if (windows_rename(old_guid, new_guid) != 0) { - log_error(errno, "Error renaming '%s' to '%s'. %s.\n", old_guid, new_guid, strerror(errno)); - return -1; - } - - /* the rename is the latest operation, and then we don't care about the links, we just remove both */ - windows_delete_link(fss, old_name); - windows_delete_link(fss, new_name); - - return 0; -} - -void fssnapshot_unmount(const struct fssnapshot_struct* fss) -{ - /* - * Remove symlinks on exit to prevent "Dangling Links." - * Since \Device\HarddiskVolumeShadowCopyN paths are reassigned by the kernel - * at boot, a stale link would either point to a non-existent device or, - * worse, a different disk's snapshot. - * - * Deleting the link ensures system hygiene and prevents users from seeing - * "Location not available" errors or incorrect data after a reboot. - */ - windows_delete_link(fss, SNAPSHOT_PENDING); - windows_delete_link(fss, SNAPSHOT_STABLE); -} - /****************************************************************************/ /* pthread like interface */ @@ -2654,6 +2198,49 @@ int windows_cond_wait(windows_cond_t* cond, windows_mutex_t* mutex) return 0; } +int windows_rwlock_init(windows_rwlock_t* rwlock, void* attr) +{ + (void)attr; + InitializeSRWLock(&rwlock->srw); + rwlock->exclusive = 0; + + return 0; +} + +int windows_rwlock_destroy(windows_rwlock_t* rwlock) +{ + (void)rwlock; + + return 0; +} + +int windows_rwlock_rdlock(windows_rwlock_t* rwlock) +{ + AcquireSRWLockShared(&rwlock->srw); + + return 0; +} + +int windows_rwlock_wrlock(windows_rwlock_t* rwlock) +{ + AcquireSRWLockExclusive(&rwlock->srw); + rwlock->exclusive = 1; + + return 0; +} + +int windows_rwlock_unlock(windows_rwlock_t* rwlock) +{ + if (rwlock->exclusive) { + rwlock->exclusive = 0; + ReleaseSRWLockExclusive(&rwlock->srw); + } else { + ReleaseSRWLockShared(&rwlock->srw); + } + + return 0; +} + struct windows_key_context { void (*func)(void*); DWORD key; @@ -2668,8 +2255,10 @@ int windows_key_create(windows_key_t* key, void (*destructor)(void*)) struct windows_key_context* context; context = malloc(sizeof(struct windows_key_context)); - if (!context) + if (!context) { + errno = ENOMEM; return -1; + } context->func = destructor; context->key = TlsAlloc(); @@ -2688,21 +2277,21 @@ int windows_key_create(windows_key_t* key, void (*destructor)(void*)) return 0; } -int windows_key_delete(windows_key_t void_key) +int windows_key_delete(windows_key_t key) { - struct windows_key_context* key = void_key; + struct windows_key_context* context = key; /* use the destructor for the local variable and remove from the list of destructors */ - if (key->func) { - void* value = windows_getspecific(key); + if (context->func) { + void* value = windows_getspecific(context); if (value) - key->func(value); - tommy_list_remove_existing(&windows_key_list, &key->node); + context->func(value); + tommy_list_remove_existing(&windows_key_list, &context->node); } - TlsFree(key->key); + TlsFree(context->key); - free(key); + free(context); return 0; } @@ -2764,8 +2353,10 @@ int windows_create(thread_id_t* thread, void* attr, void* (*func)(void*), void* (void)attr; context = malloc(sizeof(struct windows_thread_context)); - if (!context) + if (!context) { + errno = ENOMEM; return -1; + } context->func = func; context->arg = arg; @@ -2806,9 +2397,844 @@ int windows_join(thread_id_t thread, void** retval) return 0; } +/****************************************************************************/ +/* exec */ + +#define COMMAND_LINE_MAX 32767 + +static int needs_quote(const WCHAR* arg) +{ + while (*arg) { + if (*arg == L' ' || *arg == L'\t' || *arg == L'"') + return 1; + ++arg; + } + + return 0; +} + +#define charcat(c) \ + do { \ + if (pos + 1 >= size) { \ + return -1; \ + } \ + cmd[pos++] = (c); \ + } while (0) + +static int fixcat(WCHAR* cmd, int size, int pos, const WCHAR* arg) +{ + while (*arg) + charcat(*arg++); + + return pos; +} + +static int argcat(WCHAR* cmd, int size, int pos, const WCHAR* arg) +{ + int has_quote; + + /* space separator */ + if (pos != 0) + charcat(L' '); + + has_quote = needs_quote(arg); + + if (!has_quote) { + while (*arg) + charcat(*arg++); + } else { + /* starting quote */ + charcat(L'"'); + + while (*arg) { + int bl = 0; + while (*arg == L'\\') { + ++arg; + ++bl; + } + + if (*arg == 0) { + /* double backslashes before closing quote */ + bl = bl * 2; + while (bl--) + charcat(L'\\'); + } else if (*arg == '"') { + /* double backslashes + escape the quote */ + bl = bl * 2 + 1; + while (bl--) + charcat(L'\\'); + charcat(L'"'); + ++arg; + } else { + /* normal backslashes */ + while (bl--) + charcat(L'\\'); + charcat(*arg); + ++arg; + } + } + + /* ending quote */ + charcat(L'"'); + } + + return pos; +} + +pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const char* run_as_user) +{ + wchar_t conv[CONV_MAX]; + HANDLE stdout_write_handle = INVALID_HANDLE_VALUE; + HANDLE stdout_read_handle = INVALID_HANDLE_VALUE; + HANDLE stderr_write_handle = INVALID_HANDLE_VALUE; + HANDLE stderr_read_handle = INVALID_HANDLE_VALUE; + SECURITY_ATTRIBUTES sa; + PROCESS_INFORMATION pi; + STARTUPINFOW si; + BOOL ret; + int has_out = (stdout_read_int != NULL); + int has_err = (stderr_read_int != NULL); + int out_f = -1; + int err_f = -1; + + /* set the bInheritHandle flag so pipe handles are inherited */ + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = TRUE; + sa.lpSecurityDescriptor = NULL; + + if (has_out) { + /* create a pipe for the child process's STDOUT */ + if (!CreatePipe(&stdout_read_handle, &stdout_write_handle, &sa, 0)) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to create pipe for spawn, errno=%s(%d)", strerror(errno), errno); + return -1; + } + + /* ensure the reading handle to the pipe is not inherited */ + if (!SetHandleInformation(stdout_read_handle, HANDLE_FLAG_INHERIT, 0)) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to handle information for spawn, errno=%s(%d)", strerror(errno), errno); + CloseHandle(stdout_write_handle); + CloseHandle(stdout_read_handle); + return -1; + } + + out_f = _open_osfhandle((intptr_t)stdout_read_handle, O_RDONLY | O_BINARY); + if (out_f == -1) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to open osfhandle for spawn, errno=%s(%d)", strerror(errno), errno); + CloseHandle(stdout_write_handle); + CloseHandle(stdout_read_handle); + return -1; + } + } + + if (has_err) { + /* create a pipe for the child process's STDERR */ + if (!CreatePipe(&stderr_read_handle, &stderr_write_handle, &sa, 0)) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to create pipe for spawn, errno=%s(%d)", strerror(errno), errno); + if (has_out) { + CloseHandle(stdout_write_handle); + close(out_f); + } + return -1; + } + + /* ensure the reading handle to the pipe is not inherited */ + if (!SetHandleInformation(stderr_read_handle, HANDLE_FLAG_INHERIT, 0)) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to handle information for spawn, errno=%s(%d)", strerror(errno), errno); + CloseHandle(stderr_write_handle); + CloseHandle(stderr_read_handle); + if (has_out) { + CloseHandle(stdout_write_handle); + close(out_f); + } + return -1; + } + + err_f = _open_osfhandle((intptr_t)stderr_read_handle, O_RDONLY | O_BINARY); + if (err_f == -1) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to open osfhandle for spawn, errno=%s(%d)", strerror(errno), errno); + CloseHandle(stderr_write_handle); + CloseHandle(stderr_read_handle); + if (has_out) { + CloseHandle(stdout_write_handle); + close(out_f); + } + return -1; + } + } + + /* prepare command line string (Windows uses a single string, not an array) */ + WCHAR cmd_buffer[COMMAND_LINE_MAX]; + char cmd_buffer_conv[COMMAND_LINE_MAX * 3]; /* * 3 is needed because a single UTF-16 character can take up to 3 bytes in UTF-8 */ + int pos = 0; + for (int i = 0; argv[i]; ++i) { + pos = argcat(cmd_buffer, COMMAND_LINE_MAX, pos, u8tou16(conv, argv[i])); + if (pos < 0) { + os_syslog(OS_LVL_INFO, "command to long for spawn"); + if (has_out) { + CloseHandle(stdout_write_handle); + close(out_f); + } + if (has_err) { + CloseHandle(stderr_write_handle); + close(err_f); + } + return -1; + } + } + cmd_buffer[pos] = 0; + + /* set up members of the STARTUPINFO structure */ + ZeroMemory(&pi, sizeof(pi)); + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + si.hStdOutput = has_out ? stdout_write_handle : GetStdHandle(STD_OUTPUT_HANDLE); + si.hStdError = has_err ? stderr_write_handle : GetStdHandle(STD_ERROR_HANDLE); + si.dwFlags |= STARTF_USESTDHANDLES; + + /* + * Set the Working Directory to the root of the C drive. + * For safety, we avoid defaulting to C:\Windows\System32. + */ + const wchar_t* cwd = L"C:\\"; + + /* create the child process */ + if (run_as_user == 0 || run_as_user[0] == 0) { + ret = CreateProcessW( + NULL, + cmd_buffer, + NULL, NULL, + TRUE, /* inherit pipe handles */ + CREATE_NEW_PROCESS_GROUP, + NULL, cwd, + &si, &pi + ); + } else { + /* Drop to restricted service account */ + HANDLE h_token = NULL; + + /* Validate that the requested user is actually a supported Service Account before attempting logon */ + if (_stricmp(run_as_user, "LocalService") != 0 && _stricmp(run_as_user, "NetworkService") != 0) { + os_syslog(OS_LVL_INFO, "only supported users are LocalService and NetworkService"); + if (has_out) { + CloseHandle(stdout_write_handle); + close(out_f); + } + if (has_err) { + CloseHandle(stderr_write_handle); + close(err_f); + } + return -1; + } + + if (!LogonUserW(u8tou16(conv, run_as_user), L"NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, LOGON32_PROVIDER_DEFAULT, &h_token)) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to logon user %s, errno=%s(%d)", run_as_user, strerror(errno), errno); + if (has_out) { + CloseHandle(stdout_write_handle); + close(out_f); + } + if (has_err) { + CloseHandle(stderr_write_handle); + close(err_f); + } + return -1; + } + + /* Create an environment block to ensure PATH is loaded */ + LPVOID env = NULL; + if (!CreateEnvironmentBlock(&env, h_token, FALSE)) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to get user %s environment, errno=%s(%d)", run_as_user, strerror(errno), errno); + CloseHandle(h_token); + if (has_out) { + CloseHandle(stdout_write_handle); + close(out_f); + } + if (has_err) { + CloseHandle(stderr_write_handle); + close(err_f); + } + return -1; + } + + ret = CreateProcessAsUserW( + h_token, + NULL, + cmd_buffer, + NULL, NULL, + TRUE, /* inherit pipe handles */ + CREATE_NEW_PROCESS_GROUP | CREATE_UNICODE_ENVIRONMENT, + env, cwd, + &si, &pi + ); + + if (env) + DestroyEnvironmentBlock(env); + CloseHandle(h_token); + } + if (!ret) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to create process '%s' for spawn, errno=%s(%d)", u16tou8_force(cmd_buffer_conv, sizeof(cmd_buffer_conv), cmd_buffer, wcslen(cmd_buffer) + 1, 0), strerror(errno), errno); + if (has_out) { + CloseHandle(stdout_write_handle); + close(out_f); + } + if (has_err) { + CloseHandle(stderr_write_handle); + close(err_f); + } + return -1; + } + + /* close the write end of the pipes in the parent */ + if (has_out) { + CloseHandle(stdout_write_handle); + } + if (has_err) { + CloseHandle(stderr_write_handle); + } + + /* close the handle to the primary thread, we don't need it */ + CloseHandle(pi.hThread); + + if (has_out) { + *stdout_read_int = out_f; + } + if (has_err) { + *stderr_read_int = err_f; + } + + return (intptr_t)pi.hProcess; +} + +pid_t os_wait(pid_t pid, int* status) +{ + HANDLE h = (void*)pid; + DWORD exit_code; + + WaitForSingleObject(h, INFINITE); + + if (GetExitCodeProcess(h, &exit_code)) { + CloseHandle(h); + *status = exit_code; + return pid; + } + + CloseHandle(h); + + return -1; +} + +int os_term(pid_t pid) +{ + HANDLE h = (void*)pid; + DWORD id = GetProcessId(h); + + if (id == 0) + return -1; + + /* detach from current console (if any) */ + FreeConsole(); + + /* attach to the child's console */ + if (!AttachConsole(id)) + return -1; + + /* Disable Ctrl-C for the PARENT so we don't kill ourselves */ + SetConsoleCtrlHandler(0, TRUE); + + /* This will now reach the child's SetConsoleCtrlHandler */ + GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, id); + + /* Clean up */ + FreeConsole(); + + return 0; +} + +int os_command(const char* command, const char* run_as_user, const char* stdin_text) +{ + wchar_t conv[CONV_MAX]; + HANDLE stdin_read_handle; + HANDLE stdin_write_handle; + SECURITY_ATTRIBUTES sa; + PROCESS_INFORMATION pi; + STARTUPINFOW si; + BOOL ret; + int64_t start, stop; + + start = os_tick_sec(); + + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = TRUE; + sa.lpSecurityDescriptor = NULL; + + /* create pipe for child's STDIN */ + if (!CreatePipe(&stdin_read_handle, &stdin_write_handle, &sa, 0)) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to create pipe for command, errno=%s(%d)", strerror(errno), errno); + return -1; + } + + /* ensure the parent's write end is NOT inherited */ + if (!SetHandleInformation(stdin_write_handle, HANDLE_FLAG_INHERIT, 0)) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to handle information for spawn, errno=%s(%d)", strerror(errno), errno); + CloseHandle(stdin_read_handle); + CloseHandle(stdin_write_handle); + return -1; + } + + /* create a handle to the NUL device */ + HANDLE nul = CreateFileW( + L"NUL", + GENERIC_WRITE, + FILE_SHARE_WRITE | FILE_SHARE_READ, + &sa, + OPEN_EXISTING, + 0, + NULL); + if (nul == INVALID_HANDLE_VALUE) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to create nul device, errno=%s(%d)", strerror(errno), errno); + CloseHandle(stdin_read_handle); + CloseHandle(stdin_write_handle); + return -1; + } + + ZeroMemory(&pi, sizeof(pi)); + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + si.hStdInput = stdin_read_handle; + si.hStdOutput = nul; + si.hStdError = nul; + si.dwFlags |= STARTF_USESTDHANDLES; + + /* + * Set the Working Directory to the root of the C drive. + * For safety, we avoid defaulting to C:\Windows\System32. + */ + const wchar_t* cwd = L"C:\\"; + + /* + * No drop of privilege requested + * Run exactly as the parent daemon + */ + if (run_as_user == 0 || run_as_user[0] == 0) { + /* create the child process */ + ret = CreateProcessW( + NULL, + u8tou16(conv, command), + NULL, NULL, + TRUE, /* inherit pipe handles */ + CREATE_NO_WINDOW, + NULL, cwd, + &si, &pi + ); + } else { + /* + * Drop to restricted service account. + */ + HANDLE h_token = NULL; + + /* + * Validate that the requested user is actually a supported + * Service Account before attempting logon. + */ + if (_stricmp(run_as_user, "LocalService") != 0 && _stricmp(run_as_user, "NetworkService") != 0) { + os_syslog(OS_LVL_INFO, "only supported users are LocalService and NetworkService"); + CloseHandle(stdin_read_handle); + CloseHandle(stdin_write_handle); + CloseHandle(nul); + return -1; + } + + if (!LogonUserW(u8tou16(conv, run_as_user), L"NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, LOGON32_PROVIDER_DEFAULT, &h_token)) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to logon user %s, errno=%s(%d)", run_as_user, strerror(errno), errno); + CloseHandle(stdin_read_handle); + CloseHandle(stdin_write_handle); + CloseHandle(nul); + return -1; + } + + /* + * Create an environment block to ensure PATH is loaded. + */ + LPVOID env = NULL; + if (!CreateEnvironmentBlock(&env, h_token, FALSE)) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to get user %s environment, errno=%s(%d)", run_as_user, strerror(errno), errno); + CloseHandle(stdin_read_handle); + CloseHandle(stdin_write_handle); + CloseHandle(nul); + CloseHandle(h_token); + return -1; + } + + ret = CreateProcessAsUserW( + h_token, + NULL, + u8tou16(conv, command), + NULL, NULL, + TRUE, /* inherit pipe handles */ + CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, + env, cwd, + &si, &pi + ); + + if (env) + DestroyEnvironmentBlock(env); + CloseHandle(h_token); + } + if (!ret) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to create process '%s' for command, errno=%s(%d)", command, strerror(errno), errno); + CloseHandle(stdin_read_handle); + CloseHandle(stdin_write_handle); + CloseHandle(nul); + return -1; + } + + /* close nul device */ + CloseHandle(nul); + + /* close the read end in the parent immediately */ + CloseHandle(stdin_read_handle); + + /* write the string to the child's STDIN */ + if (stdin_text && strlen(stdin_text) > 0) { + DWORD written; + WriteFile(stdin_write_handle, stdin_text, (DWORD)strlen(stdin_text), &written, NULL); + } + + /* closing the write handle sends EOF to the child */ + CloseHandle(stdin_write_handle); + + /* wait for completion and get exit code */ + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD status; + GetExitCodeProcess(pi.hProcess, &status); + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + + stop = os_tick_sec(); + int64_t execution_time = stop - start; + if (execution_time > 30) + os_syslog(OS_LVL_WARNING, "command %s ran for %" PRId64 " seconds that is unexpectedly long", command, execution_time); + + if (WIFEXITED(status)) { + int exit_code = WEXITSTATUS(status); + if (exit_code == 0) + os_syslog(OS_LVL_INFO, "command %s terminated in %" PRId64 " seconds with success", command, execution_time); + else + os_syslog(OS_LVL_INFO, "command %s terminated in %" PRId64 " seconds with exit code %d", command, execution_time, exit_code); + return exit_code; + } else if (WIFSIGNALED(status)) { + /* child died from a signal */ + int sig = WTERMSIG(status); + os_syslog(OS_LVL_INFO, "command %s terminated in %" PRId64 " seconds with signal %s(%d)", command, execution_time, os_signal_name(sig), sig); + return 128 + sig; + } else { + /* in Windows it can happen */ + os_syslog(OS_LVL_INFO, "command %s terminated in %" PRId64 " seconds for unknown reason, status=0x%08x", command, execution_time, (unsigned)status); + return -1; + } +} + +static WCHAR* env_combine(const WCHAR* base_env, char** envp) +{ + size_t base_len = 0; + if (base_env) { + const WCHAR* p = base_env; + while (*p != 0) { + p += wcslen(p) + 1; + } + base_len = p - base_env + 1; + } else { + base_len = 1; + } + + size_t envv_w_total_len = 0; + int envv_count = 0; + if (envp) { + while (envp[envv_count] != NULL) { + envv_count++; + } + } + + WCHAR** envv_w = NULL; + WCHAR* new_env = NULL; + + if (envv_count > 0) { + envv_w = calloc(envv_count, sizeof(WCHAR*)); + if (!envv_w) { + goto bail; + } + for (int i = 0; i < envv_count; ++i) { + wchar_t conv[CONV_MAX]; + u8tou16(conv, envp[i]); + size_t len = wcslen(conv); + envv_w[i] = malloc((len + 1) * sizeof(WCHAR)); + if (!envv_w[i]) { + errno = ENOMEM; + goto bail; + } + memcpy(envv_w[i], conv, (len + 1) * sizeof(WCHAR)); + envv_w_total_len += len + 1; + } + } + + size_t total_len = base_len + envv_w_total_len + 1; + new_env = malloc(total_len * sizeof(WCHAR)); + if (!new_env) { + errno = ENOMEM; + goto bail; + } + + WCHAR* dst = new_env; + if (base_len > 1 && base_env) { + memcpy(dst, base_env, (base_len - 1) * sizeof(WCHAR)); + dst += base_len - 1; + } + for (int i = 0; i < envv_count; ++i) { + if (envv_w && envv_w[i]) { + size_t len = wcslen(envv_w[i]); + memcpy(dst, envv_w[i], (len + 1) * sizeof(WCHAR)); + dst += len + 1; + } + } + *dst = 0; + +bail: + if (envv_w) { + for (int i = 0; i < envv_count; ++i) { + free(envv_w[i]); + } + free(envv_w); + } + + return new_env; +} + +int os_script(char** argv, char** envp, const char* run_as_user) +{ + wchar_t conv[CONV_MAX]; + PROCESS_INFORMATION pi; + STARTUPINFOW si; + BOOL ret; + char resolved_path[PATH_MAX]; + int64_t start, stop; + const char* script_path = argv[0]; + + /* resolve the script path to prevent symlink attacks */ + if (!realpath(script_path, resolved_path)) { + os_syslog(OS_LVL_INFO, "failed to resolve script, path=%s, errno=%s(%d)", script_path, strerror(errno), errno); + return -1; + } + + start = os_tick_sec(); + + ZeroMemory(&pi, sizeof(pi)); + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + + /* prepare command line string (Windows uses a single string, not an array) */ + WCHAR cmd_buffer[COMMAND_LINE_MAX]; + char cmd_buffer_conv[COMMAND_LINE_MAX * 3]; /* * 3 is needed because a single UTF-16 character can take up to 3 bytes in UTF-8 */ + int pos = 0; + + /* + * We add an extra set of quotes: cmd /c " "path with spaces" arg1 "arg 2" " + * This ensures cmd.exe parses the internal quotes correctly. + */ + pos = fixcat(cmd_buffer, COMMAND_LINE_MAX, pos, L"cmd.exe /c \" "); + pos = argcat(cmd_buffer, COMMAND_LINE_MAX, pos, u8tou16(conv, resolved_path)); + if (pos < 0) { + os_syslog(OS_LVL_INFO, "command to long for script"); + return -1; + } + for (int i = 1; argv[i]; ++i) { + pos = argcat(cmd_buffer, COMMAND_LINE_MAX, pos, u8tou16(conv, argv[i])); + if (pos < 0) { + os_syslog(OS_LVL_INFO, "command to long for script"); + return -1; + } + } + pos = fixcat(cmd_buffer, COMMAND_LINE_MAX, pos, L" \""); + if (pos < 0) { + os_syslog(OS_LVL_INFO, "command to long for script"); + return -1; + } + cmd_buffer[pos] = 0; + + /* + * Set the Working Directory to the root of the C drive. + * For safety, we avoid defaulting to C:\Windows\System32. + */ + const wchar_t* cwd = L"C:\\"; + + /* + * No drop of privilege requested + * Run exactly as the parent daemon + */ + if (run_as_user == 0 || run_as_user[0] == 0) { + WCHAR* base_env = NULL; + WCHAR* combined_env = NULL; + DWORD creation_flags = CREATE_NO_WINDOW; + + if (envp != NULL) { + base_env = GetEnvironmentStringsW(); + if (!base_env) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to get environment strings, errno=%s(%d)", strerror(errno), errno); + return -1; + } + combined_env = env_combine(base_env, envp); + FreeEnvironmentStringsW(base_env); + if (!combined_env) { + errno = ENOMEM; + os_syslog(OS_LVL_INFO, "failed to combine environment strings (out of memory)"); + return -1; + } + creation_flags |= CREATE_UNICODE_ENVIRONMENT; + } + + /* create the child process */ + ret = CreateProcessW( + NULL, + cmd_buffer, + NULL, NULL, + FALSE, /* no need to inherit handles */ + creation_flags, + combined_env, cwd, + &si, &pi + ); + + if (combined_env) + free(combined_env); + } else { + /* + * Drop to restricted service account. + */ + HANDLE h_token = NULL; + + /* + * Validate that the requested user is actually a supported + * Service Account before attempting logon. + */ + if (_stricmp(run_as_user, "LocalService") != 0 && _stricmp(run_as_user, "NetworkService") != 0) { + os_syslog(OS_LVL_INFO, "only supported users are LocalService and NetworkService"); + return -1; + } + + if (!LogonUserW(u8tou16(conv, run_as_user), L"NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, LOGON32_PROVIDER_DEFAULT, &h_token)) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to logon user %s, errno=%s(%d)", run_as_user, strerror(errno), errno); + return -1; + } + + /* + * Create an environment block to ensure PATH is loaded. + */ + LPVOID env = NULL; + if (!CreateEnvironmentBlock(&env, h_token, FALSE)) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to get user %s environment, errno=%s(%d)", run_as_user, strerror(errno), errno); + CloseHandle(h_token); + return -1; + } + + WCHAR* combined_env = NULL; + if (envp != NULL) { + combined_env = env_combine((const WCHAR*)env, envp); + if (!combined_env) { + errno = ENOMEM; + os_syslog(OS_LVL_INFO, "failed to combine environment strings (out of memory)"); + DestroyEnvironmentBlock(env); + CloseHandle(h_token); + return -1; + } + } + + ret = CreateProcessAsUserW( + h_token, + NULL, + cmd_buffer, + NULL, NULL, + FALSE, /* no need to inherit handles */ + CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, + combined_env ? combined_env : env, cwd, + &si, &pi + ); + + if (combined_env) + free(combined_env); + if (env) + DestroyEnvironmentBlock(env); + CloseHandle(h_token); + } + if (!ret) { + windows_errno(GetLastError()); + os_syslog(OS_LVL_INFO, "failed to create process '%s' for script, errno=%s(%d)", u16tou8_force(cmd_buffer_conv, sizeof(cmd_buffer_conv), cmd_buffer, wcslen(cmd_buffer) + 1, 0), strerror(errno), errno); + return -1; + } + + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD status; + GetExitCodeProcess(pi.hProcess, &status); + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + + stop = os_tick_sec(); + int64_t execution_time = stop - start; + if (execution_time > 30) + os_syslog(OS_LVL_WARNING, "script %s took %" PRId64 " seconds", resolved_path, execution_time); + + if (WIFEXITED(status)) { + int exit_code = WEXITSTATUS(status); + if (exit_code == 0) + os_syslog(OS_LVL_INFO, "script %s terminated in %" PRId64 " seconds with success", resolved_path, execution_time); + else + os_syslog(OS_LVL_INFO, "script %s terminated in %" PRId64 " seconds with exit code %d", resolved_path, execution_time, exit_code); + return exit_code; + } else if (WIFSIGNALED(status)) { + /* child died from a signal */ + int sig = WTERMSIG(status); + os_syslog(OS_LVL_INFO, "script %s terminated in %" PRId64 " seconds with signal %s(%d)", resolved_path, execution_time, os_signal_name(sig), sig); + return 128 + sig; + } else { + /* in Windows it can happen */ + os_syslog(OS_LVL_INFO, "script %s terminated in %" PRId64 " seconds for unknown reason, status=0x%08x", resolved_path, execution_time, (unsigned)status); + return -1; + } +} + /****************************************************************************/ /* os */ +int os_randomize(void* void_ptr, size_t size) +{ + unsigned char* ptr = void_ptr; + + /* try RtlGenRandom */ + if (ptr_RtlGenRandom != 0 && ptr_RtlGenRandom(ptr, size) != 0) + return 0; + + return -1; +} + char* os_fgets(char* s, int size, OS_FILE* stream) { return fgets(s, size, stream); @@ -2870,25 +3296,26 @@ uint64_t os_tick(void) return r; } +uint64_t os_tick_sec(void) +{ + return GetTickCount64() / 1000; +} + uint64_t os_tick_ms(void) { return GetTickCount64(); } -int os_randomize(void* void_ptr, size_t size) +void os_privileges_acquire(void) { - size_t i; - unsigned char* ptr = void_ptr; +} - /* try RtlGenRandom */ - if (ptr_RtlGenRandom != 0 && ptr_RtlGenRandom(ptr, size) != 0) - return 0; +void os_privileges_release(void) +{ +} - /* fallback to standard rand */ - for (i = 0; i < size; ++i) - ptr[i] = random_u8(); - - return 0; +void os_privileges_drop(void) +{ } void os_abort(void) @@ -2916,6 +3343,11 @@ void os_abort(void) printf("https://github.com/amadvance/snapraid/issues\n"); /* use exit() and not abort to avoid the Windows abort dialog */ + os_exit(); +} + +void os_exit(void) +{ exit(EXIT_FAILURE); } @@ -2945,40 +3377,40 @@ static void exedir_init(void) slash[1] = 0; } -void os_init(int opt) +void os_init(unsigned opt) { HMODULE ntdll, kernel32; - is_scan_winfind = opt != 0; + is_scan_winfind = (opt & OS_INIT_OPT_WINFIND) != 0; /* initialize the thread local storage for strerror(), using free() as destructor */ if (windows_key_create(&last_error, free) != 0) { - log_fatal(EEXTERNAL, "Error calling windows_key_create().\n"); - exit(EXIT_FAILURE); + os_syslog(OS_LVL_CRITICAL, "error calling windows_key_create()"); + os_exit(); } tick_last = 0; if (windows_mutex_init(&tick_lock, 0) != 0) { - log_fatal(EEXTERNAL, "Error calling windows_mutex_init().\n"); - exit(EXIT_FAILURE); + os_syslog(OS_LVL_CRITICAL, "error calling windows_mutex_init()"); + os_exit(); } ntdll = GetModuleHandle("NTDLL.DLL"); if (!ntdll) { - log_fatal(EEXTERNAL, "Error loading the NTDLL module.\n"); - exit(EXIT_FAILURE); + os_syslog(OS_LVL_CRITICAL, "error loading the NTDLL module"); + os_exit(); } kernel32 = GetModuleHandle("KERNEL32.DLL"); if (!kernel32) { - log_fatal(EEXTERNAL, "Error loading the KERNEL32 module.\n"); - exit(EXIT_FAILURE); + os_syslog(OS_LVL_CRITICAL, "error loading the KERNEL32 module"); + os_exit(); } dll_advapi32 = LoadLibrary("ADVAPI32.DLL"); if (!dll_advapi32) { - log_fatal(EEXTERNAL, "Error loading the ADVAPI32 module.\n"); - exit(EXIT_FAILURE); + os_syslog(OS_LVL_CRITICAL, "error loading the ADVAPI32 module"); + os_exit(); } /* check for Wine presence */ @@ -2987,13 +3419,15 @@ void os_init(int opt) /* get pointer to RtlGenRandom, note that it was reported missing in some cases */ ptr_RtlGenRandom = (void*)GetProcAddress(dll_advapi32, "SystemFunction036"); - /* - * Set the thread execution level to avoid sleep - * first try for Windows 7 - */ - if (SetThreadExecutionState(WIN32_ES_CONTINUOUS | WIN32_ES_SYSTEM_REQUIRED | WIN32_ES_AWAYMODE_REQUIRED) == 0) { - /* retry with the XP variant */ - SetThreadExecutionState(WIN32_ES_CONTINUOUS | WIN32_ES_SYSTEM_REQUIRED); + if ((opt & OS_INIT_OPT_AVOID_SLEEP) != 0) { + /* + * Set the thread execution level to avoid sleep + * first try for Windows 7 + */ + if (SetThreadExecutionState(WIN32_ES_CONTINUOUS | WIN32_ES_SYSTEM_REQUIRED | WIN32_ES_AWAYMODE_REQUIRED) == 0) { + /* retry with the XP variant */ + SetThreadExecutionState(WIN32_ES_CONTINUOUS | WIN32_ES_SYSTEM_REQUIRED); + } } exedir_init(); diff --git a/cmdline/mingw.h b/os/mingw.h similarity index 74% rename from cmdline/mingw.h rename to os/mingw.h index f6ba3931..5282b14e 100644 --- a/cmdline/mingw.h +++ b/os/mingw.h @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2011 Andrea Mazzoleni +// Copyright (C) 2026 Andrea Mazzoleni #ifndef __PORTABLE_MINGW_H #define __PORTABLE_MINGW_H @@ -8,6 +8,14 @@ #include +#define SYSLOG "EventLog" + +#define OS_LVL_CRITICAL 1 +#define OS_LVL_ERROR 2 +#define OS_LVL_WARNING 3 +#define OS_LVL_INFO 4 +#define OS_LVL_DEBUG 5 + /* map Windows name to POSIX name */ #define strncasecmp _strnicmp @@ -39,13 +47,44 @@ wchar_t* convert_arg(wchar_t* conv_buf, const char* src, int only_if_required); #define convert(buf, a) convert_arg(buf, a, 0) #define convert_if_required(buf, a) convert_arg(buf, a, 1) +/** + * Convert a generic string from UTF16 to UTF8. + * \param conv_buf Destination UTF-8 buffer of size CONV_MAX. + * \param src Source UTF-16 null-terminated string. + * \return The converted string. It never fails. + */ char* u16tou8(char* conv_buf, const wchar_t* src); /** * Convert a generic string from UTF8 to UTF16. + * \param conv_buf Destination UTF-16 buffer of size CONV_MAX. + * \param src Source UTF-8 null-terminated string. + * \return The converted string. It never fails. */ wchar_t* u8tou16(wchar_t* conv_buf, const char* src); +/** + * Convert a generic string from UTF16 to UTF8. + * \param conv_buf Destination UTF-8 buffer. + * \param number_of_char Size of the destination buffer in bytes. + * \param src Source UTF-16 string. + * \param number_of_wchar Number of wide characters to convert. + * \param result_length_without_terminator Optional pointer to receive the converted length. + * \return The converted string, or 0 on failure. + */ +char* u16tou8_mayfail(char* conv_buf, size_t number_of_char, const wchar_t* src, size_t number_of_wchar, size_t* result_length_without_terminator); + +/** + * Convert a generic string from UTF8 to UTF16. + * \param conv_buf Destination UTF-16 buffer. + * \param conv_size Size of the destination buffer in wchar_t elements. + * \param src Source UTF-8 string. + * \param number_of_char Number of bytes to convert (or -1 for null-terminated string). + * \param result_length_without_terminator Optional pointer to receive the converted length. + * \return The converted string, or 0 on failure. + */ +wchar_t* u8tou16_mayfail(wchar_t* conv_buf, size_t number_of_wchar, const char* src, size_t number_of_char, size_t* result_length_without_terminator); + /****************************************************************************/ /* file */ @@ -58,6 +97,8 @@ wchar_t* u8tou16(wchar_t* conv_buf, const char* src); /* Remap functions and types */ #undef fopen #define fopen windows_fopen +#define FOPEN_TEXT "t" +#define FOPEN_BINARY "b" #undef open #define open windows_open #define open_noatime windows_open @@ -106,6 +147,7 @@ wchar_t* u8tou16(wchar_t* conv_buf, const char* src); #undef utimensat #define utimensat windows_utimensat #define O_NOFOLLOW 0 +#define O_CLOEXEC 0 #define dirent_hidden windows_dirent_hidden #define HAVE_STRUCT_DIRENT_D_STAT 1 #undef HAVE_STRUCT_DIRENT_D_INO @@ -145,6 +187,12 @@ wchar_t* u8tou16(wchar_t* conv_buf, const char* src); #define HAVE_DIRECT_IO 1 #define O_DIRECT 0x10000000 #define O_DSYNC 0x20000000 +#undef gmtime_r +#define gmtime_r windows_gmtime_r +#undef localtime_r +#define localtime_r windows_localtime_r +#undef realpath +#define realpath windows_realpath /** * If nanoseconds are not supported, we report the special STAT_NSEC_INVALID value, @@ -155,6 +203,39 @@ wchar_t* u8tou16(wchar_t* conv_buf, const char* src); /* We have nano second support */ #define STAT_NSEC(st) ((int)(st)->st_mtimensec) +static inline int WEXITSTATUS(DWORD status) +{ + return status; +} + +static inline int WTERMSIG(DWORD status) +{ + /* identify most common crash reasons */ + switch (status) { + case 0x40000015 : return SIGABRT; /* STATUS_FATAL_APP_EXIT */ + case 0xC0000005 : return SIGSEGV; /* STATUS_ACCESS_VIOLATION */ + case 0xC0000006 : return SIGSEGV; /* STATUS_IN_PAGE_ERROR */ + case 0xC000001D : return SIGILL; /* STATUS_ILLEGAL_INSTRUCTION */ + case 0xC00000FD : return SIGSEGV; /* STATUS_STACK_OVERFLOW */ + case 0xC0000090 : return SIGFPE; /* STATUS_FLOAT_INVALID_OPERATION */ + case 0xC0000094 : return SIGFPE; /* STATUS_INTEGER_DIVIDE_BY_ZERO */ + case 0xC000013A : return SIGINT; /* STATUS_CONTROL_C_EXIT */ + case 0xC0000409 : return SIGSEGV; /* STATUS_STACK_BUFFER_OVERRUN */ + } + + return 0; +} + +static inline int WIFEXITED(DWORD status) +{ + return status < 0xC0000000 && WTERMSIG(status) == 0; +} + +static inline int WIFSIGNALED(DWORD status) +{ + return WTERMSIG(status) != 0; +} + /** * Generic stat information. */ @@ -354,7 +435,7 @@ const char* windows_stat_desc(struct stat* st); unsigned windows_sleep(unsigned seconds); /** - * List usleep(). + * Like usleep(). */ void windows_usleep(uint64_t useconds); @@ -411,10 +492,25 @@ ssize_t windows_pread(int f, void* buffer, size_t size, off_t offset); ssize_t windows_pwrite(int f, const void* buffer, size_t size, off_t offset); /** - * List direct_size(). + * Like direct_size(). */ size_t windows_direct_size(void); +/** + * Like gmtime_r(). + */ +struct tm* windows_gmtime_r(const time_t* timer, struct tm* result); + +/** + * Like localtime_r(). + */ +struct tm* windows_localtime_r(const time_t* timer, struct tm* result); + +/** + * Like realpath(). + */ +char* windows_realpath(const char* path, char* resolved_path); + /** * Like GetFileAttributes() */ @@ -448,17 +544,28 @@ int windows_is_wine(void); #define HAVE_THREAD 1 typedef void* windows_thread_t; typedef CRITICAL_SECTION windows_mutex_t; +typedef struct { + SRWLOCK srw; + BOOL exclusive; +} windows_rwlock_t; typedef CONDITION_VARIABLE windows_cond_t; typedef void* windows_key_t; /* remap to pthread */ +#define pid_t intptr_t #define thread_id_t windows_thread_t #define thread_mutex_t windows_mutex_t #define thread_cond_t windows_cond_t +#define thread_rwlock_t windows_rwlock_t #define thread_key_t windows_key_t #define pthread_mutex_init windows_mutex_init #define pthread_mutex_destroy windows_mutex_destroy #define pthread_mutex_lock windows_mutex_lock #define pthread_mutex_unlock windows_mutex_unlock +#define pthread_rwlock_init windows_rwlock_init +#define pthread_rwlock_destroy windows_rwlock_destroy +#define pthread_rwlock_rdlock windows_rwlock_rdlock +#define pthread_rwlock_wrlock windows_rwlock_wrlock +#define pthread_rwlock_unlock windows_rwlock_unlock #define pthread_cond_init windows_cond_init #define pthread_cond_destroy windows_cond_destroy #define pthread_cond_signal windows_cond_signal @@ -478,6 +585,11 @@ int windows_mutex_init(windows_mutex_t* mutex, void* attr); int windows_mutex_destroy(windows_mutex_t* mutex); int windows_mutex_lock(windows_mutex_t* mutex); int windows_mutex_unlock(windows_mutex_t* mutex); +int windows_rwlock_init(windows_rwlock_t* mutex, void* attr); +int windows_rwlock_destroy(windows_rwlock_t* mutex); +int windows_rwlock_rdlock(windows_rwlock_t* mutex); +int windows_rwlock_wrlock(windows_rwlock_t* mutex); +int windows_rwlock_unlock(windows_rwlock_t* mutex); int windows_cond_init(windows_cond_t* cond, void* attr); int windows_cond_destroy(windows_cond_t* cond); int windows_cond_signal(windows_cond_t* cond); @@ -496,6 +608,23 @@ int windows_key_delete(windows_key_t key); void* windows_getspecific(windows_key_t key); int windows_setspecific(windows_key_t key, void* value); +/****************************************************************************/ +/* service */ + +#if !HAVE_GETOPT_LONG +#error HAVE_GETOPT_LONG is required in Windows for the service options +#endif + +/** + * Log into Eventlog + */ +void windows_eventlog(int level, const char* msg); + +/** + * Signal that the service is starting and it needs more time. + */ +void windows_starting(void); + #endif #endif diff --git a/os/os.h b/os/os.h new file mode 100644 index 00000000..37e6c758 --- /dev/null +++ b/os/os.h @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2026 Andrea Mazzoleni + +#ifndef __OS_H +#define __OS_H + +/****************************************************************************/ +/* syslog */ + +/** + * Log an informational syslog message. + * The message should start with a lowercase letter and must not terminate with \n. + * \param level One of OS_LVL_*. + * \param format Printf-like format string. + */ +void os_syslog(int level, const char* format, ...) __attribute__((format(attribute_printf, 2, 3))); + +/****************************************************************************/ +/* signal */ + +/** + * Enable or disable signal handling. + * \param enable 1 to enable signals, 0 to disable. + */ +void os_signal_set(int enable); + +/** + * Initialize signal handling. + * \param handler_term Signal handler callback for termination signals (SIGTERM, SIGINT). + * \param handler_hup Signal handler callback for hangup signal (SIGHUP). + */ +void os_signal_init(void (*handler_term)(int sig), void (*handler_hup)(int sig)); + +/** + * Restore signal handlers after fork in child process. + * This resets signals to default handling. + */ +void os_signal_restore_after_fork(void); + +/** + * Get string representation of signal number. + * \param sig Signal number. + * \return Signal name string. + */ +const char* os_signal_name(int sig); + +/** + * Global variable to identify if Ctrl+C is pressed. + * \return 1 if Ctrl+C was pressed/interrupted, 0 otherwise. + */ +int os_signal_interrupt(void); + +/****************************************************************************/ +/* exec */ + +/** + * Opaque handle tracking a process execution stream and its process ID. + * + * This structure encapsulates a standard buffered I/O stream tied to the + * redirected standard output of a child process spawned via os_popen(). + */ +#ifdef __MINGW32__ +#define OS_FILE FILE +#else +typedef struct OS_FILE { + FILE* fp; /**< Standard I/O buffered stream wrapper. */ + pid_t pid; /**< Process ID of the child process. */ +} OS_FILE; +#endif + +/** + * Spawns a child process and opens a buffered stream to read its stdout. + * + * This function acts as a safer alternative to standard popen(), accepting an + * explicit argument vector rather than evaluating a shell command string. + * + * The child process is completely isolated in its own process group (via setpgid) + * with its stdin and stderr redirected to /dev/null. The underlying pipe is + * created with O_CLOEXEC set to prevent leakage across concurrent forks. + * + * \param argv A NULL-terminated array of strings representing the argument vector. + * argv[0] must contain the absolute path to the verified executable. + * \return A pointer to an initialized OS_FILE structure on success, or NULL on failure. + */ +OS_FILE* os_popen(const char** argv); + +/** + * Reads a line from an OS execution stream. + * + * Reads characters from the specified stream into the buffer string until + * (size - 1) characters are read, a newline character is read and transferred, + * or an end-of-file (EOF) condition is encountered. A terminating null + * character is appended. + * + * \param s Pointer to an array of chars where the string read is stored. + * \param size Maximum number of characters to be read (including the null character). + * \param stream Pointer to the OS_FILE tracking context. + * \return On success, returns the buffer pointer s. If EOF is reached or a read + * error occurs before any characters are read, returns NULL. + */ +char* os_fgets(char* s, int size, OS_FILE* stream); + +/** + * Closes an execution stream and reaps the associated child process. + * + * Closes the underlying buffered standard I/O stream, releases the allocated + * tracking context container, and blocks until the associated child process + * terminates to prevent the creation of zombie processes. + * + * \param stream Pointer to the active OS_FILE context to close and free. + * \return The termination status of the child process on success, or -1 on failure. + */ +int os_pclose(OS_FILE* stream); + +/** + * Spawn a new process with the specified argument vector, optionally capturing stdout and/or stderr. + * \param argv Array of command line arguments. + * \param stdout_read_fd Pointer to store file descriptor for stdout, or NULL to redirect to /dev/null. + * \param stderr_read_fd Pointer to store file descriptor for stderr, or NULL to redirect to /dev/null. + * \param run_as_user User to run script as (NULL for current user). + * \return Process ID of spawned process, or -1 on failure. + */ +pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char* run_as_user); + +/** + * Wait for the child process to terminate. + * \param pid Process ID of the child process. + * \param status Pointer to store the exit status. + * \return Child PID on success, -1 on failure. + */ +pid_t os_wait(pid_t pid, int* status); + +/** + * Terminate gracefully a process. + * \param pid Process ID of the process to terminate. + * \return 0 on success, -1 on failure. + */ +int os_term(pid_t pid); + +/** + * Fork and execute a verified executable, discarding all I/O. + * + * Spawns @argv[0] in a new process with stdin, stdout and stderr all + * redirected to /dev/null. Use this for fire-and-forget tasks where the + * child's output is not needed. + * + * The child is placed in its own process group (setpgid) to isolate it + * from signals sent to the daemon's process group. + * + * \param argv NULL-terminated argument vector. argv[0] must be the absolute path + * to the executable. + * \return The child exit status on success, or -1 on failure. + */ +int os_spawn_and_wait(const char** argv); + +/** + * Execute a system command with optional user context and input. + * \param command Command to execute. + * \param run_as_user User to run command as (NULL for current user). + * \param stdin_text Text to provide as stdin (NULL for no input). + * \return Exit status of command. + */ +int os_command(const char* command, const char* run_as_user, const char* stdin_text); + +/** + * Execute a script file with specified user context. + * \param argv Array of command line arguments. + * \param envp Environment variables (NULL-terminated list of strings). + * \param run_as_user User to run script as (NULL for current user). + * \return Exit status of script. + */ +int os_script(char** argv, char** envp, const char* run_as_user); + +/** + * Validates a string for exec. + * \param str The input string to validate. + * \return -1 if dangerous characters are detected, 0 otherwise. + */ +int os_validate_exec_input(const char* str); + +/****************************************************************************/ +/* fs */ + +/** + * Physical offset not yet read. + */ +#define FILEPHY_UNREAD_OFFSET 0 + +/** + * Special value returned when the file-system doesn't report any offset for unknown reason. + */ +#define FILEPHY_UNREPORTED_OFFSET 1 + +/** + * Special value returned when the file doesn't have a real offset. + * For example, because it's stored in the NTFS MFT. + */ +#define FILEPHY_WITHOUT_OFFSET 2 + +/** + * Value indicating real offsets. All offsets greater or equal at this one are real. + */ +#define FILEPHY_REAL_OFFSET 3 + +/** + * Get the physical address of the specified file. + * This is expected to be just a hint and not necessarily correct or unique. + * \param path The path of the file. + * \param size The size of the file. + * \param physical Pointer to store the physical offset. + * \return 0 on success, -1 on failure. + */ +int filephy(const char* path, uint64_t size, uint64_t* physical); + +/****************************************************************************/ +/* privileges */ + +/** + * Bracketed Privileges System: + * These functions allow the daemon to run with dropped effective privileges + * by default (euid/egid set to an unprivileged user like "nobody"), while + * temporarily escalating to root privileges for specific operations that require + * administrative permissions (e.g., config changes, executing SnapRAID commands, + * managing log files). + */ + +/** + * Temporarily acquire root privileges. + * Restores the effective user and group ID of the calling thread to root (0). + */ +void os_privileges_acquire(void); + +/** + * Release root privileges. + * Reverts the effective user and group ID of the calling thread to the unprivileged credentials. + */ +void os_privileges_release(void); + +/** + * Drop effective privileges permanently to an unprivileged user (e.g., "nobody"). + * Called after startup/initialization is complete to transition the daemon into + * the Bracketed Privileges execution mode. + */ +void os_privileges_drop(void); + +/****************************************************************************/ +/* os */ + +#define OS_INIT_OPT_AVOID_SLEEP 1 /**< Prevent the system to go to sleep */ +#define OS_INIT_OPT_WINFIND 2 /**< Use the legacy Windows FindFirstFile interface instead of the new stream interface */ + +/** + * Initializes the system. + * \param opt System initialization options. + */ +void os_init(unsigned opt); + +/** + * Deinitializes the system. + */ +void os_done(void); + +/** + * Get the os_tick counter value. + * + * Note that the frequency is unspecified, because the time measure + * is meant to be used to compare the ratio between usage times. + * \return Monotonic clock tick value. + */ +uint64_t os_tick(void); + +/** + * Get the os_tick counter value in millisecond. + * \return Monotonic clock value in milliseconds. + */ +uint64_t os_tick_ms(void); + +/** + * Get the os_tick counter value in seconds. + * \return Monotonic clock value in seconds. + */ +uint64_t os_tick_sec(void); + +/** + * Abort the process with a stacktrace. + */ +void os_abort(void) __noreturn; + +/** + * Exit the process with failure. + */ +void os_exit(void) __noreturn; + +/** + * Shutdown the system. + * \return 0 on success, -1 on failure. + */ +int os_shutdown(void); + +/** + * Clear the screen. + */ +void os_clear(void); + +/** + * Fill memory with pseudo-random values + * \param ptr Pointer to the memory buffer. + * \param size Size of the memory buffer. + * \return 0 on success, -1 on failure. + */ +int os_randomize(void* ptr, size_t size); + +/** + * Get the optimal CPU for the speed test + * \return Optimal CPU index. + */ +int os_get_optimal_cpu(void); + +#endif diff --git a/cmdline/portable.h b/os/portable.h similarity index 89% rename from cmdline/portable.h rename to os/portable.h index a371975a..4dc0460e 100644 --- a/cmdline/portable.h +++ b/os/portable.h @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2011 Andrea Mazzoleni +// Copyright (C) 2026 Andrea Mazzoleni #ifndef __PORTABLE_H #define __PORTABLE_H @@ -32,6 +32,11 @@ */ #define _WIN32_WINNT 0x601 +/** + * Enable the rand_s() function. + */ +#define _CRT_RAND_S + /** * Undef as it clashes with windows.h declarations */ @@ -46,7 +51,7 @@ #ifdef __MINGW32__ #if defined(__USE_MINGW_ANSI_STDIO) && __USE_MINGW_ANSI_STDIO == 1 #define attribute_printf gnu_printf /* GNU format */ -#define printf __mingw_printf +#define printf(...) __mingw_printf(__VA_ARGS__) /* to support %z in printf */ #else #define attribute_printf ms_printf /* MSVCRT format */ #endif @@ -65,7 +70,6 @@ #define __noreturn __attribute__((noreturn)) #endif - /** * Architecture for inline assembly. */ @@ -225,6 +229,38 @@ #include #endif +#ifdef HAVE_STRINGS +#include +#endif + +#if HAVE_SYSLOG_H +#include +#endif + +#if HAVE_GRP_H +#include +#endif + +#if HAVE_PWD_H +#include +#endif + +#if HAVE_SYS_SYSINFO_H +#include +#endif + +#if HAVE_SYS_UTSNAME_H +#include +#endif + +#if HAVE_ZLIB +#include +#endif + +#if HAVE_ZSTD +#include +#endif + #ifdef __APPLE__ #include #include @@ -252,20 +288,6 @@ #include #endif -/* implement close_range for glibc 2.33 or earlier */ -#if defined(__linux__) && !defined(HAVE_CLOSE_RANGE) -#include -#ifndef __NR_close_range -#define __NR_close_range 436 -#endif -#define close_range close_range_impl -static inline int close_range_impl(unsigned int first, unsigned int last, unsigned int flags) -{ - return (int)syscall(__NR_close_range, first, last, flags); -} -#define HAVE_CLOSE_RANGE 1 -#endif - /* * Flags for open() */ diff --git a/os/unix.c b/os/unix.c new file mode 100644 index 00000000..3753a115 --- /dev/null +++ b/os/unix.c @@ -0,0 +1,1607 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2026 Andrea Mazzoleni + +#include "os/portable.h" + +#ifndef __MINGW32__ /* Only for Unix */ + +#include "os.h" + +/****************************************************************************/ +/* signal */ + +void os_signal_restore_after_fork(void) +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + sigaction(SIGTERM, &sa, 0); + sigaction(SIGINT, &sa, 0); + sigaction(SIGHUP, &sa, 0); + /* do not restore SIGPIPE */ + + /* ensure signals are unblocked */ + sigset_t mask; + sigemptyset(&mask); + sigprocmask(SIG_SETMASK, &mask, NULL); /* cannot use pthread_sigmask after fork */ +} + +void os_signal_init(void (*handler_term)(int sig), void (*handler_hup)(int sig)) +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handler_term; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; /* use the SA_RESTART to automatically restart interrupted system calls */ + + sigaction(SIGTERM, &sa, 0); + sigaction(SIGINT, &sa, 0); + + sa.sa_handler = handler_hup; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; /* use the SA_RESTART to automatically restart interrupted system calls */ + + sigaction(SIGHUP, &sa, 0); + + sa.sa_handler = SIG_IGN; /* ignore the signal */ + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGPIPE, &sa, 0); +} + +void os_signal_set(int enable) +{ + sigset_t set; + + sigemptyset(&set); + sigaddset(&set, SIGTERM); + sigaddset(&set, SIGINT); + sigaddset(&set, SIGHUP); + + pthread_sigmask(enable ? SIG_UNBLOCK : SIG_BLOCK, &set, 0); +} + +const char* os_signal_name(int sig) +{ + switch (sig) { +#ifdef SIGHUP + case SIGHUP : return "SIGHUP"; +#endif +#ifdef SIGINT + case SIGINT : return "SIGINT"; +#endif +#ifdef SIGQUIT + case SIGQUIT : return "SIGQUIT"; +#endif +#ifdef SIGILL + case SIGILL : return "SIGILL"; +#endif +#ifdef SIGTRAP + case SIGTRAP : return "SIGTRAP"; +#endif +#ifdef SIGABRT + case SIGABRT : return "SIGABRT"; +#endif +#ifdef SIGBUS + case SIGBUS : return "SIGBUS"; +#endif +#ifdef SIGFPE + case SIGFPE : return "SIGFPE"; +#endif +#ifdef SIGKILL + case SIGKILL : return "SIGKILL"; +#endif +#ifdef SIGUSR1 + case SIGUSR1 : return "SIGUSR1"; +#endif +#ifdef SIGSEGV + case SIGSEGV : return "SIGSEGV"; +#endif +#ifdef SIGUSR2 + case SIGUSR2 : return "SIGUSR2"; +#endif +#ifdef SIGPIPE + case SIGPIPE : return "SIGPIPE"; +#endif +#ifdef SIGALRM + case SIGALRM : return "SIGALRM"; +#endif +#ifdef SIGTERM + case SIGTERM : return "SIGTERM"; +#endif + } + + return "UNKNOWN"; +} + +/****************************************************************************/ +/* fs */ + +#if !HAVE_EACCESS +int eaccess(const char* pathname, int mode) +{ + return faccessat(AT_FDCWD, pathname, mode, AT_EACCESS); +} +#endif + +#if HAVE_POSIX_FADVISE +int posix_fadvise_wrapper(int fd, off_t offset, off_t len, int advice) +{ + int ret = posix_fadvise(fd, offset, len, advice); + + if (ret == ENOENT) + return ENOSYS; + + return ret; +} +#endif + +int open_noatime(const char* file, int flags) +{ +#ifdef O_NOATIME + int f = open(file, flags | O_NOATIME); + + /* only root is allowed to use O_NOATIME, in case retry without it */ + if (f == -1 && errno == EPERM) + f = open(file, flags); + return f; +#else + return open(file, flags); +#endif +} + +int dirent_hidden(struct dirent* dd) +{ + return dd->d_name[0] == '.'; +} + +size_t direct_size(void) +{ + long size; + + size = sysconf(_SC_PAGESIZE); + + if (size == -1) { + /* LCOV_EXCL_START */ + os_syslog(OS_LVL_CRITICAL, "no page size"); + os_exit(); + /* LCOV_EXCL_STOP */ + } + + return size; +} + +const char* stat_desc(struct stat* st) +{ + if (S_ISREG(st->st_mode)) + return "regular"; + if (S_ISDIR(st->st_mode)) + return "directory"; + if (S_ISCHR(st->st_mode)) + return "character"; + if (S_ISBLK(st->st_mode)) + return "block-device"; + if (S_ISFIFO(st->st_mode)) + return "fifo"; + if (S_ISLNK(st->st_mode)) + return "symlink"; + if (S_ISSOCK(st->st_mode)) + return "socket"; + return "unknown"; +} + +int filephy(const char* path, uint64_t size, uint64_t* physical) +{ +#if HAVE_LINUX_FIEMAP_H + /* + * In Linux get the real physical address of the file + * Note that FIEMAP doesn't require root permission + */ + int f; + struct fiemap* fiemap; + size_t fiemap_size; + unsigned int blknum; + + /* + * First try with FIEMAP + * if works for ext2, ext3, ext4, xfs, btrfs + */ + fiemap_size = sizeof(struct fiemap) + sizeof(struct fiemap_extent); + fiemap = malloc(fiemap_size); + if (!fiemap) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + f = open(path, O_RDONLY); + if (f == -1) { + /* LCOV_EXCL_START */ + free(fiemap); + return -1; + /* LCOV_EXCL_STOP */ + } + + memset(fiemap, 0, fiemap_size); + fiemap->fm_start = 0; + fiemap->fm_length = ~0ULL; + fiemap->fm_flags = FIEMAP_FLAG_SYNC; /* required to ensure that just created files report a valid address and not 0 */ + fiemap->fm_extent_count = 1; /* we are interested only at the first block */ + + if (ioctl(f, FS_IOC_FIEMAP, fiemap) != -1) { + uint32_t flags = fiemap->fm_extents[0].fe_flags; + uint64_t offset = fiemap->fm_extents[0].fe_physical; + + /* check some condition for validating the offset */ + if (flags & FIEMAP_EXTENT_DATA_INLINE) { + /* if the data is inline, we don't have an offset to report */ + *physical = FILEPHY_WITHOUT_OFFSET; + } else if (flags & FIEMAP_EXTENT_UNKNOWN) { + /* if the offset is unknown, we don't have an offset to report */ + *physical = FILEPHY_WITHOUT_OFFSET; + } else if (offset == 0) { + /* + * 0 is the general fallback for file-systems when + * they don't have an offset to report + */ + *physical = FILEPHY_WITHOUT_OFFSET; + } else { + /* finally report the real offset */ + *physical = offset + FILEPHY_REAL_OFFSET; + } + + free(fiemap); + + if (close(f) == -1) + return -1; + return 0; + } + + free(fiemap); + + /* if the file is empty, FIBMAP doesn't work, and we don't even try to use it */ + if (size == 0) { + *physical = FILEPHY_WITHOUT_OFFSET; + if (close(f) == -1) + return -1; + return 0; + } + + /* + * Then try with FIBMAP + * it works for jfs, reiserfs, ntfs-3g + * in exfat it always returns 0, that it's anyway better than the fake inodes + */ + blknum = 0; /* first block */ + if (ioctl(f, FIBMAP, &blknum) != -1) { + *physical = blknum + FILEPHY_REAL_OFFSET; + if (close(f) == -1) + return -1; + return 0; + } + + /* + * Otherwise don't use anything, and keep the directory traversal order + * at now this should happen only for vfat + * and it's surely better than using fake inodes + */ + *physical = FILEPHY_UNREPORTED_OFFSET; + if (close(f) == -1) + return -1; +#else + /* + * In a generic Unix use a dummy value for all the files + * We don't want to risk to use the inode without knowing + * if it really improves performance. + * In this way we keep them in the directory traversal order + * that at least keeps files in the same directory together. + * Note also that in newer file-system with snapshot, like ZFS, + * the inode doesn't represent even more the disk position, because files + * are not overwritten in place, but rewritten in another location + * of the disk. + */ + *physical = FILEPHY_UNREPORTED_OFFSET; + + (void)path; /* not used here */ + (void)size; +#endif + + return 0; +} + +/****************************************************************************/ +/* exec */ + +/* + * Scrubbed environment + * Only provide the bare essentials. + */ +static char* const envp_scrubbed[] = { + "PATH=" +#ifdef __APPLE__ + "/opt/homebrew/bin:" +#endif + "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "TERM=dumb", + "LANG=C", + "IFS= \t\n", + NULL +}; + +/* + * Enforce a strict 128-byte shebang limit to match the actual Linux kernel behavior + * and prevent security risks from truncation. + * + * The Linux kernel reads exactly 128 bytes (BINPRM_BUF_SIZE internal = 128) to parse + * the shebang line. Longer lines are silently truncated, which can cause the kernel + * to execute an unintended or malformed interpreter path. + * + * Although the UAPI header exposes 256 and other platforms (macOS, BSD) allow longer + * lines, we deliberately enforce the conservative 128-byte limit on *all* platforms + * to eliminate any possibility of a mismatch between our validation and runtime + * execution on Linux, and to avoid subtle truncation-based attacks. + * + * Effective maximum shebang line length: ~126 characters (accounting for "#!", whitespace, + * interpreter path, optional argument, and newline). + */ +#define SHEBANG_MAX (128 + 2) /* extra space for end-of-line and final 0 */ + +/** + * Verifies that the shebang interpreter is in an allowed list of paths. + * This prevents attacks where a malicious script uses an attacker-controlled interpreter. + */ +static int verify_shebang_interpreter(int fd, const char* script_path) +{ + char shebang[SHEBANG_MAX]; + ssize_t bytes_read; + char* interpreter; + char* args; + struct stat st; + + /* list of allowed interpreter paths */ + const char* allowed_interpreters[] = { + /* shells - system */ + "/bin/sh", + "/usr/bin/sh", + "/bin/bash", + "/usr/bin/bash", + "/bin/zsh", + "/usr/bin/zsh", + "/bin/dash", + "/usr/bin/dash", + + /* shells - homebrew (Apple Silicon) */ +#ifdef __APPLE__ + "/opt/homebrew/bin/sh", + "/opt/homebrew/bin/bash", + "/opt/homebrew/bin/zsh", + "/opt/homebrew/bin/dash", +#endif + + /* shells - homebrew (Intel macOS legacy) */ + "/usr/local/bin/bash", + "/usr/local/bin/zsh", + "/usr/local/bin/dash", + + /* python - system */ + "/usr/bin/python3", + "/usr/bin/python", + + /* python - Homebrew */ +#ifdef __APPLE__ + "/opt/homebrew/bin/python3", + "/opt/homebrew/bin/python", +#endif + "/usr/local/bin/python3", + "/usr/local/bin/python", + + /* perl */ + "/usr/bin/perl", +#ifdef __APPLE__ + "/opt/homebrew/bin/perl", +#endif + "/usr/local/bin/perl", + + /* ruby */ + "/usr/bin/ruby", +#ifdef __APPLE__ + "/opt/homebrew/bin/ruby", +#endif + "/usr/local/bin/ruby", + + /* node.js */ + "/usr/bin/node", +#ifdef __APPLE__ + "/opt/homebrew/bin/node", +#endif + "/usr/local/bin/node", + 0 + }; + + bytes_read = pread(fd, shebang, sizeof(shebang) - 1, 0); /* reserve space for the terminating 0 */ + if (bytes_read < 0) { + os_syslog(OS_LVL_INFO, "failed to read script shebang, path=%s, errno=%s(%d)", script_path, strerror(errno), errno); + return -1; + } + if (bytes_read < 4) { + os_syslog(OS_LVL_INFO, "script %s is too small or missing a shebang", script_path); + return -1; + } + shebang[bytes_read] = 0; + + /* check for shebang */ + if (shebang[0] != '#' || shebang[1] != '!') { + os_syslog(OS_LVL_INFO, "script %s is missing shebang (#!)", script_path); + return -1; + } + + char* end_of_line = strchr(shebang, '\n'); + if (!end_of_line || end_of_line - shebang > 128) { + os_syslog(OS_LVL_INFO, "script %s has invalid or overlong shebang (#!), exceeds 126 characters", script_path); + return -1; + } + *end_of_line = 0; + + /* skip "#!" and whitespace */ + interpreter = shebang + 2; + while (*interpreter && isspace((unsigned char)*interpreter)) + ++interpreter; + + if (*interpreter == 0) { + os_syslog(OS_LVL_INFO, "script %s has empty shebang", script_path); + return -1; + } + + /* separate interpreter from arguments */ + args = interpreter; + while (*args && !isspace((unsigned char)*args)) + ++args; + if (*args) + *args++ = 0; /* terminate interpreter */ + + /* check if interpreter is in allowed list */ + int found = 0; + for (int i = 0; allowed_interpreters[i] != 0; ++i) { + if (strcmp(interpreter, allowed_interpreters[i]) == 0) { + found = 1; + break; + } + } + + if (!found) { + os_syslog(OS_LVL_INFO, "script %s uses disallowed interpreter %s", script_path, interpreter); + return -1; + } + + /* verify interpreter exists and is safe */ + if (stat(interpreter, &st) != 0) { + os_syslog(OS_LVL_INFO, "interpreter %s does not exist, errno=%s(%d)", interpreter, strerror(errno), errno); + return -1; + } + + /* interpreter must be a regular file */ + if (!S_ISREG(st.st_mode)) { + os_syslog(OS_LVL_INFO, "interpreter %s must be a regular file", interpreter); + return -1; + } + + /* interpreter must be owned by root */ + if (st.st_uid != 0) { + os_syslog(OS_LVL_INFO, "interpreter %s not owned by root", interpreter); + return -1; + } + + /* interpreter must be not world-writable */ + if (st.st_mode & S_IWOTH) { + os_syslog(OS_LVL_INFO, "interpreter %s is world-writable", interpreter); + return -1; + } + + /* interpreter must be not group-writable (unless group is root) */ + if ((st.st_mode & S_IWGRP) && st.st_gid != 0) { + os_syslog(OS_LVL_INFO, "interpreter %s is group-writable by non-root group", interpreter); + return -1; + } + + /* interpreter must be executable */ + if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) { + os_syslog(OS_LVL_INFO, "interpreter %s is not executable", interpreter); + return -1; + } + + /* interpreter must be not setuid / setgid */ + if (st.st_mode & (S_ISUID | S_ISGID)) { + os_syslog(OS_LVL_INFO, "file %s has setuid/setgid bits set", interpreter); + return -1; + } + + /* all checks passed */ + return 0; +} + +/* + * Securely verify and open an executable file. + * + * Performs a series of security checks on the file at @exec_path before + * returning an open file descriptor suitable for use with fexecve(2), or + * falling back to execve(2) on systems that lack it. + * + * Security model: + * - @exec_path must be absolute. + * - The path is resolved via realpath(3) to canonicalize it and eliminate + * symlinks before any further checks are performed. + * - The parent directory is opened with O_PATH | O_NOFOLLOW to pin its + * inode, and the file is opened with openat(2) relative to that pinned + * descriptor. This closes the TOCTOU window between path resolution + * and file open. + * - Both the parent directory and the file must be owned by root or the + * daemon's real/effective UID, must not be world-writable, and must not + * be group-writable by a group other than the daemon's real/effective GID. + * - The file must be a regular file with at least one execute bit set. + * - The setuid and setgid bits must not be set. + * - On systems with fexecve(2) support, the returned fd is opened without + * O_CLOEXEC if the file is a script (@is_script=1). This is necessary because + * fexecve(2) executes scripts by passing a descriptor path (e.g., /dev/fd/N) + * to the script interpreter, which requires the fd to remain open across + * the execve syscall. For compiled binaries, O_CLOEXEC is safely applied + * to prevent leaking the fd to the spawned process. On systems without + * fexecve(2), execve(2) is used with @resolved_path and O_CLOEXEC is + * always applied. + * + * @exec_path Absolute path to the executable to verify. + * @resolved_path Caller-allocated buffer of at least PATH_MAX bytes. On + * success, filled with the canonicalized path from realpath(3). + * + * Returns an open file descriptor (>= 0) on success. The caller is + * responsible for closing it. Returns -1 on any verification failure; + * the specific reason is emitted via os_syslog(OS_LVL_INFO, ...). + */ +static int verify_executable(const char* exec_path, char* resolved_path, int is_script) +{ + struct stat st; + uid_t process_uid, process_euid; + gid_t process_gid, process_egid; + + process_uid = getuid(); + process_euid = geteuid(); + process_gid = getgid(); + process_egid = getegid(); + + /* verify path is absolute */ + if (exec_path[0] != '/') { + os_syslog(OS_LVL_INFO, "path %s must be absolute", exec_path); + return -1; + } + + /* resolve the path to prevent symlink attacks */ + if (!realpath(exec_path, resolved_path)) { + os_syslog(OS_LVL_INFO, "failed to resolve %s, errno=%s(%d)", exec_path, strerror(errno), errno); + return -1; + } + + char* last_slash = strrchr(resolved_path, '/'); + if (last_slash == 0) { + os_syslog(OS_LVL_INFO, "relative execution of %s not allowed", resolved_path); + return -1; + } + if (last_slash == resolved_path) { + os_syslog(OS_LVL_INFO, "root dir execution of %s not allowed", resolved_path); + return -1; + } + + const char* exec_name = last_slash + 1; + if (exec_name[0] == 0) { + os_syslog(OS_LVL_INFO, "no executable name in %s", exec_path); + return -1; + } + + char dir_path[PATH_MAX]; + size_t dir_len = last_slash - resolved_path; + memcpy(dir_path, resolved_path, dir_len); + dir_path[dir_len] = 0; + + int dir_fd = open(dir_path, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (dir_fd < 0) { + os_syslog(OS_LVL_INFO, "failed to open directory %s, errno=%s(%d)", dir_path, strerror(errno), errno); + return -1; + } + + if (fstat(dir_fd, &st) != 0) { + os_syslog(OS_LVL_INFO, "failed to stat directory %s, errno=%s(%d)", dir_path, strerror(errno), errno); + close(dir_fd); + return -1; + } + + /* directory must be owned by root or the daemon's real user */ + if (st.st_uid != process_uid && st.st_uid != process_euid && st.st_uid != 0) { + os_syslog(OS_LVL_INFO, "directory %s owner must match the daemon owner or be root", dir_path); + close(dir_fd); + return -1; + } + + /* directory must be not group-writable unless group matches daemon */ + if ((st.st_mode & S_IWGRP) && st.st_gid != process_gid && st.st_gid != process_egid && st.st_gid != 0) { + os_syslog(OS_LVL_INFO, "directory %s must be not group-writable unless group matches daemon owner or root", dir_path); + close(dir_fd); + return -1; + } + + /* directory must be not world-writable */ + if (st.st_mode & S_IWOTH) { + os_syslog(OS_LVL_INFO, "directory %s must be not world-writable", dir_path); + close(dir_fd); + return -1; + } + + /* + * Open the executable + */ + int flags = O_RDONLY | O_NOFOLLOW; +#if HAVE_FEXECVE + if (!is_script) { + flags |= O_CLOEXEC; /* with fexecve cannot use O_CLOEXEC (Close on Exec) for scripts */ + } +#else + (void)is_script; + flags |= O_CLOEXEC; +#endif + + int fd = openat(dir_fd, exec_name, flags); + if (fd < 0) { + os_syslog(OS_LVL_INFO, "failed to open %s, errno=%s(%d)", resolved_path, strerror(errno), errno); + close(dir_fd); + return -1; + } + + close(dir_fd); + + /* get the file handle (TOCTOU Protection) */ + if (fstat(fd, &st) == -1) { + os_syslog(OS_LVL_INFO, "failed to stat %s, errno=%s(%d)", resolved_path, strerror(errno), errno); + close(fd); + return -1; + } + + /* ensure it's a regular file */ + if (!S_ISREG(st.st_mode)) { + os_syslog(OS_LVL_INFO, "file %s is not a regular file", resolved_path); + close(fd); + return -1; + } + + /* explicitly reject scripts if not allowed */ + if (!is_script) { + char magic[2]; + if (read(fd, magic, 2) == 2 && magic[0] == '#' && magic[1] == '!') { + os_syslog(OS_LVL_INFO, "file %s is a script, which is not supported", resolved_path); + close(fd); + return -1; + } + if (lseek(fd, 0, SEEK_SET) == (off_t)-1) { + /* ignore lseek errors on block devices or pipes, though it's a regular file here */ + } + } + + /* ensure it has execute permissions */ + if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) { + os_syslog(OS_LVL_INFO, "file %s is not an executable", resolved_path); + close(fd); + return -1; + } + + /* must be owned by root or the daemon's real user */ + if (st.st_uid != process_uid && st.st_uid != process_euid && st.st_uid != 0) { + os_syslog(OS_LVL_INFO, "file %s owner must match the daemon owner or be root", resolved_path); + close(fd); + return -1; + } + + /* must be not group-writable unless group matches daemon */ + if ((st.st_mode & S_IWGRP) && st.st_gid != process_gid && st.st_gid != process_egid && st.st_gid != 0) { + os_syslog(OS_LVL_INFO, "file %s must be not group-writable unless group matches daemon owner or root", resolved_path); + close(fd); + return -1; + } + + /* must be not world-writable */ + if (st.st_mode & S_IWOTH) { + os_syslog(OS_LVL_INFO, "file %s must be not world-writable", resolved_path); + close(fd); + return -1; + } + + /* must be not setuid / setgid */ + if (st.st_mode & (S_ISUID | S_ISGID)) { + os_syslog(OS_LVL_INFO, "file %s has setuid/setgid bits set", resolved_path); + close(fd); + return -1; + } + + return fd; +} + +int os_validate_exec_input(const char* str) +{ + /* reject paths trying to go up levels */ + if (strstr(str, "..") != 0) { + os_syslog(OS_LVL_INFO, "invalid argument %s for execution", str); + return -1; + } + + /* reject inputs starting with '-' to prevent Flag Injection */ + if (str[0] == '-') { + os_syslog(OS_LVL_INFO, "invalid argument %s for execution", str); + return -1; + } + + return 0; +} + +/** + * Executes a script directly via its file descriptor. + */ +int os_script(char** argv, char** envp, const char* run_as_user) +{ + char resolved_path[PATH_MAX]; + pid_t pid; + int ret; + int status; + int64_t start, stop; + + int fd = verify_executable(argv[0], resolved_path, 1); + if (fd < 0) { + return -1; + } + + if (verify_shebang_interpreter(fd, resolved_path) != 0) { + close(fd); + return -1; + } + + start = os_tick_sec(); + + pid = fork(); + if (pid < 0) { + os_syslog(OS_LVL_INFO, "failed to fork script=%s, errno=%s(%d)", resolved_path, strerror(errno), errno); + close(fd); + return -1; + } + + if (pid == 0) { + /* child process */ + + /* + * Create a new process group for the child. + * This isolates the child from signals sent to the daemon's process group + * and allows the daemon to kill this process and all its future children + * (the entire group) using kill(-pid, SIGTERM). + */ + setpgid(0, 0); + + /* drop privileges first (if configured) */ + if (run_as_user && run_as_user[0] != 0) { + errno = 0; + struct passwd* pw = getpwnam(run_as_user); + if (!pw) { + /* if errno is 0, user simply wasn't found. Otherwise, it's a real error */ + if (errno == 0) + _exit(127); + else + _exit(126); + } + if (initgroups(pw->pw_name, pw->pw_gid) != 0) + _exit(126); + if (setgid(pw->pw_gid) != 0) + _exit(126); + if (setuid(pw->pw_uid) != 0) + _exit(126); + } + + /* io sandboxing */ + int null_fd = open("/dev/null", O_RDWR); + if (null_fd == -1) + _exit(126); + + /* redirect stdin/out/err to /dev/null */ + if (dup2(null_fd, STDIN_FILENO) == -1 + || dup2(null_fd, STDOUT_FILENO) == -1 + || dup2(null_fd, STDERR_FILENO) == -1) + _exit(126); + + /* if the fd we opened is not one of the standard ones, close it */ + if (null_fd > STDERR_FILENO) + close(null_fd); + +#if defined(CLOSE_RANGE_CLOEXEC) && defined(HAVE_CLOSE_RANGE) + /* + * Set all fd to be closed on exec as extra safety measure + * + * fallback: if it fails, we assume to be still safe, as all fds and + * sockets should be already created with CLOEXEC. + * + * For scripts we cannot set fd as CLOEXEC. + */ + close_range(3, fd - 1, CLOSE_RANGE_CLOEXEC); + close_range(fd + 1, ~0U, CLOSE_RANGE_CLOEXEC); +#endif + + /* restore and unblock signals */ + os_signal_restore_after_fork(); + + /* child will receive SIGALRM in 300 seconds (5 minutes) as a timeout */ + alarm(300); + + /* use the resolved path for execution */ + argv[0] = resolved_path; + + /* + * Direct Execution via File Descriptor + * The kernel uses the shebang in the FD to find the interpreter. + */ + if (envp != NULL) { + int envv_count = 0; + while (envp[envv_count] != NULL) { + envv_count++; + } + int scrubbed_count = sizeof(envp_scrubbed) / sizeof(envp_scrubbed[0]) - 1; + char* envp_dynamic[scrubbed_count + envv_count + 1]; + for (int i = 0; i < scrubbed_count; ++i) { + envp_dynamic[i] = envp_scrubbed[i]; + } + for (int i = 0; i < envv_count; ++i) { + envp_dynamic[scrubbed_count + i] = envp[i]; + } + envp_dynamic[scrubbed_count + envv_count] = NULL; + +#if HAVE_FEXECVE + fexecve(fd, argv, envp_dynamic); +#else + /* fallback: unfortunately must use the path */ + execve(resolved_path, argv, envp_dynamic); +#endif + } else { +#if HAVE_FEXECVE + fexecve(fd, argv, envp_scrubbed); +#else + /* fallback: unfortunately must use the path */ + execve(resolved_path, argv, envp_scrubbed); +#endif + } + _exit(127); + } + + /* parent process */ + close(fd); + + do { + ret = waitpid(pid, &status, 0); + } while (ret == -1 && errno == EINTR); + + if (ret == -1) { + os_syslog(OS_LVL_INFO, "failed to wait for script, path=%s, errno=%s(%d)", resolved_path, strerror(errno), errno); + return -1; + } + + stop = os_tick_sec(); + int64_t execution_time = stop - start; + if (execution_time > 30) + os_syslog(OS_LVL_WARNING, "script %s took %" PRId64 " seconds", resolved_path, execution_time); + + if (WIFEXITED(status)) { + int exit_code = WEXITSTATUS(status); + if (exit_code == 0) + os_syslog(OS_LVL_INFO, "script %s terminated in %" PRId64 " seconds with success", resolved_path, execution_time); + else + os_syslog(OS_LVL_INFO, "script %s terminated in %" PRId64 " seconds with exit code %d", resolved_path, execution_time, exit_code); + return exit_code; + } else if (WIFSIGNALED(status)) { + /* child died from a signal */ + int sig = WTERMSIG(status); + if (sig == SIGALRM) { + os_syslog(OS_LVL_INFO, "script %s timeout after %" PRId64 " seconds", resolved_path, execution_time); + } else { + os_syslog(OS_LVL_INFO, "script %s terminated in %" PRId64 " seconds with signal %s(%d)", resolved_path, execution_time, os_signal_name(sig), sig); + } + return 128 + sig; + } else { + /* in Linux it should never happen */ + os_syslog(OS_LVL_INFO, "script %s terminated in %" PRId64 " seconds for unknown reason, status=%d", resolved_path, execution_time, status); + return -1; + } +} + +int os_command(const char* command, const char* run_as_user, const char* stdin_text) +{ + pid_t pid; + int ret; + int status; + int pipe_fds[2] = { -1, -1 }; + int64_t start, stop; + + /* create pipe only if we have text to send */ + if (stdin_text != NULL) { + if (pipe(pipe_fds) < 0) { + os_syslog(OS_LVL_INFO, "failed to create input pipe, errno=%s(%d)", strerror(errno), errno); + return -1; + } + } + + start = os_tick_sec(); + + pid = fork(); + if (pid < 0) { + os_syslog(OS_LVL_INFO, "failed to fork command=%s, errno=%s(%d)", command, strerror(errno), errno); + if (pipe_fds[0] != -1) { + close(pipe_fds[0]); + close(pipe_fds[1]); + } + return -1; + } + + if (pid == 0) { + /* child process */ + + /* + * Create a new process group for the child. + * This isolates the child from signals sent to the daemon's process group + * and allows the daemon to kill this process and all its future children + * (the entire group) using kill(-pid, SIGTERM). + */ + setpgid(0, 0); + + if (pipe_fds[1] != -1) + close(pipe_fds[1]); /* Close unused write end */ + + /* drop privileges first (if configured) */ + if (run_as_user && run_as_user[0] != 0) { + errno = 0; + struct passwd* pw = getpwnam(run_as_user); + if (!pw) { + /* if errno is 0, user simply wasn't found. Otherwise, it's a real error */ + if (errno == 0) + _exit(127); + else + _exit(126); + } + if (initgroups(pw->pw_name, pw->pw_gid) != 0) + _exit(126); + if (setgid(pw->pw_gid) != 0) + _exit(126); + if (setuid(pw->pw_uid) != 0) + _exit(126); + } + + /* io sandboxing */ + int null_fd = open("/dev/null", O_RDWR); + if (null_fd == -1) + _exit(126); + + /* redirect STDIN: either from pipe or /dev/null */ + if (pipe_fds[0] != -1) { + if (dup2(pipe_fds[0], STDIN_FILENO) == -1) + _exit(126); + close(pipe_fds[0]); + } else { + if (dup2(null_fd, STDIN_FILENO) == -1) + _exit(126); + } + + /* Redirect STDOUT and STDERR to /dev/null */ + if (dup2(null_fd, STDOUT_FILENO) == -1 + || dup2(null_fd, STDERR_FILENO) == -1) + _exit(126); + + /* if the fd we opened is not one of the standard ones, close it */ + if (null_fd > STDERR_FILENO) + close(null_fd); + +#if defined(CLOSE_RANGE_CLOEXEC) && defined(HAVE_CLOSE_RANGE) + /* + * Set all fd to be closed on exec as extra safety measure + * + * fallback: if it fails, we assume to be still safe, as all fds and + * sockets should be already created with CLOEXEC. + */ + close_range(3, ~0U, CLOSE_RANGE_CLOEXEC); +#endif + + /* restore and unblock signals */ + os_signal_restore_after_fork(); + + /* child will receive SIGALRM in 300 seconds (5 minutes) as a timeout */ + alarm(300); + + char* const argv[] = { "sh", "-c", (char*)command, 0 }; + + execve("/bin/sh", argv, envp_scrubbed); + + _exit(127); + } + + /* parent process */ + if (pipe_fds[0] != -1) + close(pipe_fds[0]); /* close unused read end */ + + if (pipe_fds[1] != -1) { + /* write text to child's stdin */ + ssize_t len = strlen(stdin_text); + if (write(pipe_fds[1], stdin_text, len) != len) { + os_syslog(OS_LVL_INFO, "failed to write full stdin to command %s", command); + } + /* closing the pipe sends EOF to the child (e.g., tells curl data is done) */ + close(pipe_fds[1]); + } + + do { + ret = waitpid(pid, &status, 0); + } while (ret == -1 && errno == EINTR); + + if (ret == -1) { + os_syslog(OS_LVL_INFO, "failed to wait for command, command=%s, errno=%s(%d)", command, strerror(errno), errno); + return -1; + } + + stop = os_tick_sec(); + int64_t execution_time = stop - start; + if (execution_time > 30) + os_syslog(OS_LVL_WARNING, "command %s ran for %" PRId64 " seconds that is unexpectedly long", command, execution_time); + + if (WIFEXITED(status)) { + int exit_code = WEXITSTATUS(status); + if (exit_code == 0) + os_syslog(OS_LVL_INFO, "command %s terminated in %" PRId64 " seconds with success", command, execution_time); + else + os_syslog(OS_LVL_INFO, "command %s terminated in %" PRId64 " seconds with exit code %d", command, execution_time, exit_code); + return exit_code; + } else if (WIFSIGNALED(status)) { + /* child died from a signal */ + int sig = WTERMSIG(status); + if (sig == SIGALRM) { + os_syslog(OS_LVL_INFO, "command %s timeout after %" PRId64 " seconds", command, execution_time); + } else { + os_syslog(OS_LVL_INFO, "command %s terminated in %" PRId64 " seconds with signal %s(%d)", command, execution_time, os_signal_name(sig), sig); + } + return 128 + sig; + } else { + /* in Linux it should never happen */ + os_syslog(OS_LVL_INFO, "command %s terminated in %" PRId64 " seconds for unknown reason, status=%d", command, execution_time, status); + return -1; + } +} + +static int pipe_cloexec(int pipefd[2]) +{ +#ifdef HAVE_PIPE2 + return pipe2(pipefd, O_CLOEXEC); +#else + if (pipe(pipefd) < 0) + return -1; + + for (int i = 0; i < 2; i++) { + int flags = fcntl(pipefd[i], F_GETFD); + if (flags < 0) + goto bail; + + if (fcntl(pipefd[i], F_SETFD, flags | FD_CLOEXEC) < 0) + goto bail; + } + + return 0; + +bail: + close(pipefd[0]); + close(pipefd[1]); + return -1; +#endif +} + +/** + * os_spawn() - Fork and execute a verified executable, capturing stdout and/or stderr. + * + * Spawns @argv[0] in a new process. If @stdout_read_fd is not NULL, stdout is connected + * to a pipe whose read end is returned in @stdout_read_fd. If @stderr_read_fd is not NULL, + * stderr is connected to a pipe whose read end is returned in @stderr_read_fd. + * Otherwise, they are redirected to /dev/null. stdin is always redirected to /dev/null. + * + * The child is placed in its own process group (setpgid) to isolate it from signals + * sent to the daemon's process group. + * + * Returns the child PID on success, or -1 on failure. + */ +pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char* run_as_user) +{ + char resolved_path[PATH_MAX]; + int out_pipe[2]; + int err_pipe[2]; + int has_out = (stdout_read_fd != NULL); + int has_err = (stderr_read_fd != NULL); + pid_t pid; + + int fd = verify_executable(argv[0], resolved_path, 0); + if (fd < 0) { + return -1; + } + + if (has_out) { + if (pipe_cloexec(out_pipe) < 0) { + os_syslog(OS_LVL_INFO, "failed to open output pipe, errno=%s(%d)", strerror(errno), errno); + close(fd); + return -1; + } + } + + if (has_err) { + if (pipe_cloexec(err_pipe) < 0) { + os_syslog(OS_LVL_INFO, "failed to open error pipe, errno=%s(%d)", strerror(errno), errno); + if (has_out) { + close(out_pipe[0]); + close(out_pipe[1]); + } + close(fd); + return -1; + } + } + + pid = fork(); + if (pid < 0) { + os_syslog(OS_LVL_INFO, "failed to fork path=%s, errno=%s(%d)", resolved_path, strerror(errno), errno); + if (has_out) { + close(out_pipe[0]); + close(out_pipe[1]); + } + if (has_err) { + close(err_pipe[0]); + close(err_pipe[1]); + } + close(fd); + return -1; + } + + if (pid == 0) { + /* child process */ + + setpgid(0, 0); + + /* drop privileges first (if configured) */ + if (run_as_user && run_as_user[0] != 0) { + errno = 0; + struct passwd* pw = getpwnam(run_as_user); + if (!pw) { + if (errno == 0) + _exit(127); + else + _exit(126); + } + if (initgroups(pw->pw_name, pw->pw_gid) != 0) + _exit(126); + if (setgid(pw->pw_gid) != 0) + _exit(126); + if (setuid(pw->pw_uid) != 0) + _exit(126); + } + + /* io sandboxing */ + int null_fd = open("/dev/null", O_RDWR | O_CLOEXEC); + if (null_fd < 0) + _exit(126); + + /* stdin -> /dev/null */ + if (dup2(null_fd, STDIN_FILENO) < 0) + _exit(126); + + /* stdout */ + if (has_out) { + if (dup2(out_pipe[1], STDOUT_FILENO) < 0) + _exit(126); + } else { + if (dup2(null_fd, STDOUT_FILENO) < 0) + _exit(126); + } + + /* stderr */ + if (has_err) { + if (dup2(err_pipe[1], STDERR_FILENO) < 0) + _exit(126); + } else { + if (dup2(null_fd, STDERR_FILENO) < 0) + _exit(126); + } + + if (has_out) { + close(out_pipe[0]); + close(out_pipe[1]); + } + if (has_err) { + close(err_pipe[0]); + close(err_pipe[1]); + } + + if (null_fd > STDERR_FILENO) + close(null_fd); + +#if defined(CLOSE_RANGE_CLOEXEC) && defined(HAVE_CLOSE_RANGE) + /* + * Set all fd to be closed on exec as extra safety measure + * + * fallback: if it fails, we assume to be still safe, as all fds and + * sockets should be already created with CLOEXEC. + */ + close_range(3, ~0U, CLOSE_RANGE_CLOEXEC); +#endif + + /* restore and unblock signals */ + os_signal_restore_after_fork(); + + argv[0] = resolved_path; + +#if HAVE_FEXECVE + fexecve(fd, argv, envp_scrubbed); +#else + execve(resolved_path, argv, envp_scrubbed); +#endif + _exit(127); + } + + /* parent */ + close(fd); + + if (has_out) { +#ifdef F_SETPIPE_SZ + if (fcntl(out_pipe[0], F_SETPIPE_SZ, 4096) == -1) { + os_syslog(OS_LVL_INFO, "failed to set pipe size, errno=%s(%d)", strerror(errno), errno); + } +#endif + close(out_pipe[1]); + *stdout_read_fd = out_pipe[0]; + } + + if (has_err) { +#ifdef F_SETPIPE_SZ + if (fcntl(err_pipe[0], F_SETPIPE_SZ, 4096) == -1) { + os_syslog(OS_LVL_INFO, "failed to set pipe size, errno=%s(%d)", strerror(errno), errno); + } +#endif + close(err_pipe[1]); + *stderr_read_fd = err_pipe[0]; + } + + return pid; +} + +int os_wait(pid_t pid, int* status) +{ + int ret; + + do { + ret = waitpid(pid, status, 0); + } while (ret == -1 && errno == EINTR); + + if (ret == -1) { + os_syslog(OS_LVL_INFO, "failed to wait, errno=%s(%d)", strerror(errno), errno); + } + + return ret; +} + +int os_term(pid_t pid) +{ + /* + * Send signal to the negative PID to target the entire Process Group. + * This ensures that SnapRAID and any programs it may have spawned are + * terminated together, preventing orphaned worker processes. + */ + return kill(-pid, SIGTERM); +} + +int os_spawn_and_wait(const char** argv) +{ + pid_t pid = os_spawn((char**)argv, 0, 0, 0); + if (pid < 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + int status; + int ret = os_wait(pid, &status); + if (ret == -1) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } else if (WIFSIGNALED(status)) { + return 128 + WTERMSIG(status); + } + + return -1; +} + +OS_FILE* os_popen(const char** argv) +{ + int stdout_fd = -1; + OS_FILE* os_file = malloc(sizeof(OS_FILE)); + if (!os_file) + return 0; + + pid_t pid = os_spawn((char**)argv, &stdout_fd, 0, 0); + if (pid < 0) { + free(os_file); + return 0; + } + + FILE* fp = fdopen(stdout_fd, "r"); + if (!fp) { + /* LCOV_EXCL_START */ + int saved_errno = errno; + close(stdout_fd); + int status; + os_wait(pid, &status); + errno = saved_errno; + free(os_file); + return 0; + /* LCOV_EXCL_STOP */ + } + + os_file->fp = fp; + os_file->pid = pid; + + return os_file; +} + +char* os_fgets(char* s, int size, OS_FILE* stream) +{ + return fgets(s, size, stream->fp); +} + +int os_pclose(OS_FILE* stream) +{ + /* leep handles locally to free container block immediately */ + FILE* fp = stream->fp; + pid_t pid = stream->pid; + free(stream); + + /* close buffered stream, releasing the underlying fd */ + fclose(fp); + + int status = 0; + if (os_wait(pid, &status) < 0) { + return -1; + } + + return status; +} + +/****************************************************************************/ +/* privileges */ + +static uid_t unpriv_uid = -1; +static gid_t unpriv_gid = -1; +static int can_switch = 0; + +void os_privileges_drop(void) +{ + /* + * Detect if running as root + * + * Note that all calls to os_privileges_release() and os_privileges_acquire() + * are no-ops before this detection. + */ + if (getuid() == 0 || geteuid() == 0) { + /* find the unprivileged user "nobody" */ + struct passwd* pw = getpwnam("nobody"); + if (pw) { + unpriv_uid = pw->pw_uid; + unpriv_gid = pw->pw_gid; + can_switch = 1; + } + } + + os_privileges_release(); +} + +void os_privileges_acquire(void) +{ + if (can_switch) { + if (seteuid(0) != 0) { + os_syslog(OS_LVL_INFO, "failed to acquire privileges, errno=%s(%d)", strerror(errno), errno); + } + if (setegid(0) != 0) { + os_syslog(OS_LVL_INFO, "failed to acquire group privileges, errno=%s(%d)", strerror(errno), errno); + } + } +} + +void os_privileges_release(void) +{ + if (can_switch) { + if (setegid(unpriv_gid) != 0) { + os_syslog(OS_LVL_INFO, "failed to release group privileges, errno=%s(%d)", strerror(errno), errno); + } + if (seteuid(unpriv_uid) != 0) { + os_syslog(OS_LVL_INFO, "failed to release privileges, errno=%s(%d)", strerror(errno), errno); + } + } +} + +/****************************************************************************/ +/* os */ + +uint64_t os_tick(void) +{ +#if HAVE_MACH_ABSOLUTE_TIME + /* for Mac OS X */ + return mach_absolute_time(); +#elif HAVE_CLOCK_GETTIME && (defined(CLOCK_MONOTONIC) || defined(CLOCK_MONOTONIC_RAW)) + /* for Linux */ + struct timespec tv; + + /* nanosecond precision with clock_gettime() */ +#if defined(CLOCK_MONOTONIC_RAW) + if (clock_gettime(CLOCK_MONOTONIC_RAW, &tv) != 0) { +#else + if (clock_gettime(CLOCK_MONOTONIC, &tv) != 0) { +#endif + /* LCOV_EXCL_START */ + return 0; + /* LCOV_EXCL_STOP */ + } + + return tv.tv_sec * 1000000000ULL + tv.tv_nsec; +#else + /* other platforms */ + struct timeval tv; + + /* microsecond precision with gettimeofday() */ + if (gettimeofday(&tv, 0) != 0) { + /* LCOV_EXCL_START */ + return 0; + /* LCOV_EXCL_STOP */ + } + + return tv.tv_sec * 1000000ULL + tv.tv_usec; +#endif +} + +uint64_t os_tick_ms(void) +{ + struct timeval tv; + + if (gettimeofday(&tv, 0) != 0) { + /* LCOV_EXCL_START */ + return 0; + /* LCOV_EXCL_STOP */ + } + + return tv.tv_sec * 1000ULL + tv.tv_usec / 1000; +} + +uint64_t os_tick_sec(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + + return ts.tv_sec; +} + +int os_randomize(void* ptr, size_t size) +{ + int f; + ssize_t ret; + + f = open("/dev/urandom", O_RDONLY); + if (f == -1) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + ret = read(f, ptr, size); + if (ret < 0 || (size_t)ret != size) { + /* LCOV_EXCL_START */ + close(f); + return -1; + /* LCOV_EXCL_STOP */ + } + + if (close(f) != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + return 0; +} + +void os_clear(void) +{ + /* ANSI codes */ + printf("\033[H"); /* cursor at topleft */ + printf("\033[2J"); /* clear screen */ +} + +/* LCOV_EXCL_START */ +void os_abort(void) +{ +#if HAVE_BACKTRACE && HAVE_BACKTRACE_SYMBOLS + void* stack[32]; + char** messages; + unsigned size; + unsigned i; +#endif + + printf("Stacktrace of " PACKAGE " v" VERSION); +#ifdef _linux + printf(", linux"); +#endif +#ifdef __GNUC__ + printf(", gcc " __VERSION__); +#endif + printf(", %d-bit", (int)sizeof(void*) * 8); + printf(", PATH_MAX=%d", PATH_MAX); + printf("\n"); + +#if HAVE_BACKTRACE && HAVE_BACKTRACE_SYMBOLS + size = backtrace(stack, 32); + + messages = backtrace_symbols(stack, size); + + for (i = 1; i < size; ++i) { + const char* msg; + + if (messages) + msg = messages[i]; + else + msg = ""; + + printf("[bt] %02u: %s\n", i, msg); + + if (messages) { + int ret; + char addr2line[1024]; + size_t j = 0; + while (msg[j] != '(' && msg[j] != ' ' && msg[j] != 0) + ++j; + + snprintf(addr2line, sizeof(addr2line), "addr2line %p -e %.*s", stack[i], (unsigned)j, msg); + + ret = system(addr2line); + if (WIFEXITED(ret) && WEXITSTATUS(ret) != 0) + printf("exit:%d\n", WEXITSTATUS(ret)); + if (WIFSIGNALED(ret)) + printf("signal:%d\n", WTERMSIG(ret)); + } + } +#endif + + printf("Please report this error to the SnapRAID Issues:\n"); + printf("https://github.com/amadvance/snapraid/issues\n"); + + abort(); +} +/* LCOV_EXCL_STOP */ + +void os_exit(void) +{ + exit(EXIT_FAILURE); +} + +void os_init(unsigned opt) +{ + (void)opt; +} + +void os_done(void) +{ +} + +#endif + diff --git a/cmdline/unix.h b/os/unix.h similarity index 82% rename from cmdline/unix.h rename to os/unix.h index b1c22d17..be0b19e4 100644 --- a/cmdline/unix.h +++ b/os/unix.h @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2011 Andrea Mazzoleni +// Copyright (C) 2026 Andrea Mazzoleni #ifndef __UNIX_H #define __UNIX_H @@ -9,6 +9,38 @@ #define HAVE_DIRECT_IO 1 /**< Support O_DIRECT in open(). */ #endif +#define SYSLOG "syslog" + +#define OS_LVL_CRITICAL LOG_CRIT +#define OS_LVL_ERROR LOG_ERR +#define OS_LVL_WARNING LOG_WARNING +#define OS_LVL_INFO LOG_INFO +#define OS_LVL_DEBUG LOG_DEBUG + +/* implement close_range for glibc 2.33 or earlier */ +#if defined(__linux__) && !defined(HAVE_CLOSE_RANGE) +#include +#ifndef __NR_close_range +#define __NR_close_range 436 +#endif +#define close_range close_range_impl +static inline int close_range_impl(unsigned int first, unsigned int last, unsigned int flags) +{ + return (int)syscall(__NR_close_range, first, last, flags); +} +#define HAVE_CLOSE_RANGE 1 +#endif + +#ifndef WEXITSTATUS +#define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8) +#endif +#ifndef WIFEXITED +#define WIFEXITED(stat_val) (((stat_val) & 255) == 0) +#endif + +#define FOPEN_TEXT "" +#define FOPEN_BINARY "" + #define O_BINARY 0 /**< Not used in Unix. */ #define O_SEQUENTIAL 0 /**< In Unix posix_fadvise() shall be used. */ @@ -119,6 +151,7 @@ typedef pthread_t thread_id_t; typedef pthread_mutex_t thread_mutex_t; typedef pthread_cond_t thread_cond_t; typedef pthread_key_t thread_key_t; +typedef pthread_rwlock_t thread_rwlock_t; #endif #endif diff --git a/tommyds/tommy.c b/tommyds/tommy.c index 3463af50..f3c16d8c 100644 --- a/tommyds/tommy.c +++ b/tommyds/tommy.c @@ -6,7 +6,7 @@ #define tommy_calloc calloc_nofail #define tommy_free free -#include "cmdline/portable.h" +#include "os/portable.h" #include "cmdline/support.h" /* for malloc/calloc_nofail() */ #include "tommyhash.c"