macOS Tahoe changed how Time Machine manages its backup history, so TrueNAS Core’s tmprotect module can no longer tell when a backup has finished.

This post shows how to apply the Time Machine snapshot fixes to TrueNAS Core 13.3-U1.2, which includes Samba 4.19.6.

Build environment

Run the host commands in a root sh shell. The official FreeBSD 13 package server is no longer available, so the jail cannot install pkg from it. Copy the host’s working pkg-static into the jail and download packages from a mirror that still has them. The settings below keep FreeBSD’s repository signature checks enabled. Replace pool1 with the pool where your iocage jails are stored.

Check the installed Samba version, create the jail, and install the packages needed to build the module:

sudo sh
pkg query '%n-%v' samba
/usr/local/sbin/smbd --version

iocage create -n tmprotect-build -r 13.3-RELEASE dhcp=on vnet=on bpf=on boot=off
iocage start tmprotect-build

jail_root=/mnt/pool1/iocage/jails/tmprotect-build/root
mkdir -p "$jail_root/usr/local/sbin" "$jail_root/usr/local/etc/pkg/repos"
cp -p /usr/local/sbin/pkg-static "$jail_root/usr/local/sbin/"
cat > "$jail_root/usr/local/etc/pkg/repos/FreeBSD.conf" <<'EOF'
FreeBSD: {
  url: "https://mirrors.aliyun.com/freebsd-pkg/FreeBSD/13/amd64/latest",
  mirror_type: "none",
  signature_type: "fingerprints",
  fingerprints: "/usr/share/keys/pkg",
  enabled: yes
}
EOF

iocage exec tmprotect-build env IGNORE_OSVERSION=yes \
  /usr/local/sbin/pkg-static install -y \
  automake \
  bison \
  gmake \
  pkgconf \
  p5-Parse-Yapp \
  python3 \
  patchelf \
  libtool \
  openldap26-client \
  gnutls \
  jansson

The mirror’s packages were built for FreeBSD 13.5. IGNORE_OSVERSION=yes lets us install them in the FreeBSD 13.3 jail.

Copy the host’s ZFS libraries into the jail so the new module uses the same ZFS libraries as the host:

mkdir -p "$jail_root/usr/local/include/libzfs" "$jail_root/usr/local/include/libspl"

for lib in libzfs.so.4 libzfs_core.so.3 libnvpair.so.3 libuutil.so.3; do
  cp -L "/usr/local/lib/$lib" "$jail_root/usr/local/lib/"
  ln -sf "$lib" "$jail_root/usr/local/lib/${lib%.*}"
done

iocage console tmprotect-build

Sources and headers

Inside the jail, download the Samba source version used by the TrueNAS build instructions, along with the fix for reading Tahoe’s backup history and the snapshot timing fix:

exec sh
mkdir -p /root/build
cd /root/build
fetch -o samba.tar.gz https://github.com/truenas/samba/archive/4fec43c0c0155240da48c33ce7dc1abcf7d8c716.tar.gz
mkdir samba
tar -xf samba.tar.gz --strip-components=1 -C samba
fetch -o history.patch https://github.com/truenas/samba/commit/1e461935f89b1a058c9005c74e99c895a0673b81.patch
fetch -o deferred.patch https://github.com/truenas/samba/commit/3fba25ef9feb427cde1534075bb387236e38f747.patch

Prepare matching ZFS headers:

fetch -o zfs.tar.gz https://github.com/truenas/zfs/archive/4f2aa13822f0db5e9b94a76f3a6b5165a282b30b.tar.gz
mkdir zfs
tar -xf zfs.tar.gz --strip-components=1 -C zfs
cd zfs
./autogen.sh
MAKE=gmake ./configure --with-config=user
cp -R include/. zfs_config.h /usr/local/include/libzfs/
cp -R lib/libspl/include/. lib/libspl/include/os/freebsd/. /usr/local/include/libspl/
cd ..

Patches

Apply the two Samba fixes:

cd samba
patch -p1 < ../history.patch

# Remove an extra check so the second patch applies.
perl -pi \
  -e 's/if \(\(config->history_file == NULL\) \|\| \(last_snap \+ 900 > curtime\)\)/if (last_snap + 900 > curtime)/' \
  source3/modules/vfs_tmprotect.c

patch -p1 < ../deferred.patch

The following patch asks FreeBSD for the open history file’s full path and removes an argument from the rename function that Samba 4.19 does not accept:

patch -p1 <<'PATCH'
--- a/source3/modules/vfs_tmprotect.c
+++ b/source3/modules/vfs_tmprotect.c
@@ -27,0 +28,3 @@
+#ifdef FREEBSD
+#include <sys/user.h>
+#endif
@@ -305,2 +308 @@
-static bool get_history_full_path(const char *connectpath,
-				  const struct files_struct *fsp,
+static bool get_history_full_path(int fd,
@@ -309,3 +311,13 @@
-	config->history_file = talloc_asprintf(config, "%s/%s",
-					       connectpath,
-					       fsp->fsp_name->base_name);
+	struct kinfo_file info;
+	ZERO_STRUCT(info);
+	info.kf_structsize = sizeof(info);
+	if (fcntl(fd, F_KINFO, &info) == -1) {
+		DBG_ERR("Failed to resolve history path from fd %d: %s\n",
+			fd, strerror(errno));
+		return false;
+	}
+	if (info.kf_path[0] != '/') {
+		errno = ENOENT;
+		return false;
+	}
+	config->history_file = talloc_strdup(config, info.kf_path);
@@ -361 +373 @@
-	ok = get_history_full_path(handle->conn->connectpath, fsp, config);
+	ok = get_history_full_path(ret, config);
@@ -440,0 +453,4 @@
+	if (snapshot_name == NULL) {
+		DBG_ERR("talloc_asprintf() failed\n");
+		return;
+	}
@@ -491,2 +507 @@
-			      const struct smb_filename *smb_fname_dst,
-			      const struct vfs_rename_how *how)
+			      const struct smb_filename *smb_fname_dst)
@@ -502 +517 @@
-				    dstfsp, smb_fname_dst, how);
+				    dstfsp, smb_fname_dst);
PATCH

Build

Configure and build the module, then return to the host:

CPPFLAGS=-I/usr/local/include \
LDFLAGS=-Wl,--undefined-version \
./configure --with-libzfs --without-ad-dc --without-gettext

gmake vfs_tmprotect

cp bin/default/source3/modules/libvfs_module_tmprotect.so ../tmprotect.so
patchelf --set-rpath /usr/local/lib/samba4/private ../tmprotect.so
exit

Install

Back on the host, back up the original module, install the replacement, and restart SMB between backups:

cd /usr/local/lib/shared-modules/vfs
cp -p tmprotect.so "tmprotect.so.$(date +%Y%m%dT%H%M%S)"

ldd "$jail_root/root/build/tmprotect.so"
install -S -o root -g wheel -m 0755 "$jail_root/root/build/tmprotect.so" tmprotect.so
midclt call service.restart cifs