]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
CVE-2025-9640: Add torture test for inserting hole in stream
authorAndrew Walker <andrew.walker@truenas.com>
Thu, 28 Aug 2025 19:39:34 +0000 (19:39 +0000)
committerVolker Lendecke <vl@samba.org>
Thu, 16 Oct 2025 18:44:11 +0000 (18:44 +0000)
This commit adds an smb torture test for inserting a hole into
an alternate data stream and then verifying that hole contains
null bytes.

BUG: https://bugzilla.samba.org/show_bug.cgi?id=15885

Signed-off-by: Andrew Walker <andrew.walker@truenas.com>
Reviewed-by: Volker Lendecke <vl@samba.org>
source3/selftest/tests.py
source4/torture/vfs/streams_xattr.c [new file with mode: 0644]
source4/torture/vfs/vfs.c
source4/torture/wscript_build

index dad58fca5f2dc98e5794e4387261199f6d3ef23a..efba899a92046d45d9260cacf5cea14ab5860278 100755 (executable)
@@ -1163,6 +1163,7 @@ nbt = ["nbt.dgram"]
 vfs = [
     "vfs.fruit",
     "vfs.acl_xattr",
+    "vfs.streams_xattr",
     "vfs.fruit_netatalk",
     "vfs.fruit_file_id",
     "vfs.fruit_timemachine",
@@ -1359,6 +1360,8 @@ for t in tests:
             plansmbtorture4testsuite(t, "fileserver", '//$SERVER_IP/tmp -U$USERNAME%$PASSWORD')
     elif t == "vfs.acl_xattr":
         plansmbtorture4testsuite(t, "nt4_dc", '//$SERVER_IP/tmp -U$USERNAME%$PASSWORD')
+    elif t == "vfs.streams_xattr":
+        plansmbtorture4testsuite(t, "nt4_dc", '//$SERVER_IP/vfs_wo_fruit -U$USERNAME%$PASSWORD')
     elif t == "smb2.compound_find":
         plansmbtorture4testsuite(t, "fileserver", '//$SERVER/compound_find -U$USERNAME%$PASSWORD')
         plansmbtorture4testsuite(t, "fileserver", '//$SERVER_IP/tmp -U$USERNAME%$PASSWORD')
diff --git a/source4/torture/vfs/streams_xattr.c b/source4/torture/vfs/streams_xattr.c
new file mode 100644 (file)
index 0000000..0eb83e0
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+   Unix SMB/CIFS implementation.
+
+   Copyright (C) Andrew Walker (2025)
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "includes.h"
+#include "lib/cmdline/cmdline.h"
+#include "libcli/smb2/smb2.h"
+#include "libcli/smb2/smb2_calls.h"
+#include "libcli/smb/smbXcli_base.h"
+#include "torture/torture.h"
+#include "torture/vfs/proto.h"
+#include "libcli/resolve/resolve.h"
+#include "torture/util.h"
+#include "torture/smb2/proto.h"
+#include "lib/param/param.h"
+
+#define BASEDIR "smb2-testads"
+
+
+static bool get_stream_handle(struct torture_context *tctx,
+                             struct smb2_tree *tree,
+                             const char *dname,
+                             const char *fname,
+                             const char *sname,
+                             struct smb2_handle *hdl_in)
+{
+       bool ret = true;
+       NTSTATUS status;
+       struct smb2_handle fhandle = {{0}};
+       struct smb2_handle dhandle = {{0}};
+
+       torture_comment(tctx, "Create dir\n");
+
+       status = torture_smb2_testdir(tree, dname, &dhandle);
+       torture_assert_ntstatus_ok_goto(tctx, status, ret, done, "torture_smb2_testdir\n");
+
+       torture_comment(tctx, "Create file\n");
+
+       status = torture_smb2_testfile(tree, fname, &fhandle);
+       torture_assert_ntstatus_ok_goto(tctx, status, ret, done, "torture_smb2_testfile\n");
+
+       status = torture_smb2_testfile(tree, sname, hdl_in);
+       torture_assert_ntstatus_ok_goto(tctx, status, ret, done, "torture_smb2_testfile\n");
+
+done:
+       if (!smb2_util_handle_empty(fhandle)) {
+               smb2_util_close(tree, fhandle);
+       }
+       if (!smb2_util_handle_empty(dhandle)) {
+               smb2_util_close(tree, dhandle);
+       }
+       return ret;
+}
+
+static bool read_stream(struct torture_context *tctx,
+                       TALLOC_CTX *mem_ctx,
+                       struct smb2_tree *tree,
+                       struct smb2_handle *stream_hdl,
+                       off_t read_offset,
+                       size_t read_count,
+                       char **data_out,
+                       size_t *data_out_sz)
+{
+       NTSTATUS status;
+       struct smb2_read r;
+       bool ret = true;
+
+       ZERO_STRUCT(r);
+       r.in.file.handle = *stream_hdl;
+       r.in.length = read_count;
+       r.in.offset = read_offset;
+
+       status = smb2_read(tree, mem_ctx, &r);
+       torture_assert_ntstatus_ok_goto(tctx, status, ret, done, "stream read\n");
+
+       *data_out = (char *)r.out.data.data;
+       *data_out_sz = r.out.data.length;
+
+done:
+       return ret;
+}
+
+
+#define WRITE_PAYLOAD "canary"
+#define ADS_LEN 1024
+#define ADS_OFF_TAIL ADS_LEN - sizeof(WRITE_PAYLOAD)
+
+static bool test_streams_pwrite_hole(struct torture_context *tctx,
+                                    struct smb2_tree *tree)
+{
+       NTSTATUS status;
+       bool ok;
+       bool ret = true;
+       const char *dname = BASEDIR "\\testdir";
+       const char *fname = BASEDIR "\\testdir\\testfile";
+       const char *sname = BASEDIR "\\testdir\\testfile:test_stream";
+       const char *canary = "canary";
+       struct smb2_handle shandle = {{0}};
+       TALLOC_CTX *tmp_ctx = NULL;
+       char *data = NULL;
+       size_t data_sz, i;
+
+       ok = smb2_util_setup_dir(tctx, tree, BASEDIR);
+       torture_assert_goto(tctx, ok == true, ret, done, "Unable to setup testdir\n");
+
+       tmp_ctx = talloc_new(tree);
+       torture_assert_goto(tctx, tmp_ctx != NULL, ret, done, "Memory failure\n");
+
+       ok = get_stream_handle(tctx, tree, dname, fname, sname, &shandle);
+       if (!ok) {
+               // torture assert already set
+               goto done;
+       }
+
+       /*
+        * We're going to write a string at the beginning at the ADS, then write the same
+        * string at a later offset, introducing a hole in the file
+        */
+       torture_comment(tctx, "writing at varying offsets to create hole\n");
+       status = smb2_util_write(tree, shandle, WRITE_PAYLOAD, 0, sizeof(WRITE_PAYLOAD));
+       if (!NT_STATUS_IS_OK(status)) {
+               torture_comment(tctx, "Failed to write %zu bytes to "
+                   "stream at offset 0\n", sizeof(canary));
+               return false;
+       }
+
+       status = smb2_util_write(tree, shandle, WRITE_PAYLOAD, ADS_OFF_TAIL, sizeof(WRITE_PAYLOAD));
+       if (!NT_STATUS_IS_OK(status)) {
+               torture_comment(tctx, "Failed to write %zu bytes to "
+                   "stream at offset 1018\n", sizeof(canary));
+               return false;
+       }
+
+       /* Now we'll read the stream contents */
+       torture_comment(tctx, "Read stream data\n");
+       ok = read_stream(tctx, tmp_ctx, tree, &shandle, 0, ADS_LEN, &data, &data_sz);
+       if (!ok) {
+               // torture assert already set
+               goto done;
+       }
+
+       torture_assert_goto(tctx, data_sz == ADS_LEN, ret, done, "Short read on ADS\n");
+
+       /* Make sure our strings actually got written */
+       if (strncmp(data, WRITE_PAYLOAD, sizeof(WRITE_PAYLOAD)) != 0) {
+               torture_result(tctx, TORTURE_FAIL,
+                              "Payload write at beginning of file failed");
+               ret = false;
+               goto done;
+       }
+
+       if (strncmp(data + ADS_OFF_TAIL, WRITE_PAYLOAD, sizeof(WRITE_PAYLOAD)) != 0) {
+               torture_result(tctx, TORTURE_FAIL,
+                              "Payload write at end of file failed");
+               ret = false;
+               goto done;
+       }
+
+       /* Now we'll check that the hole is full of null bytes */
+       for (i = sizeof(WRITE_PAYLOAD); i < ADS_OFF_TAIL; i++) {
+               if (data[i] != '\0') {
+                       torture_comment(tctx, "idx: %zu, got 0x%02x when expected 0x00\n",
+                                       i, (uint8_t)data[i]);
+                       torture_result(tctx, TORTURE_FAIL,
+                                      "0x%08x: unexpected non-null byte in ADS read\n",
+                                      data[i]);
+                       ret = false;
+                       goto done;
+               }
+       }
+
+done:
+       talloc_free(tmp_ctx);
+
+       if (!smb2_util_handle_empty(shandle)) {
+               smb2_util_close(tree, shandle);
+       }
+
+       smb2_deltree(tree, BASEDIR);
+
+       return ret;
+}
+
+/*
+   basic testing of vfs_streams_xattr
+*/
+struct torture_suite *torture_vfs_streams_xattr(TALLOC_CTX *ctx)
+{
+       struct torture_suite *suite = torture_suite_create(ctx, "streams_xattr");
+
+       torture_suite_add_1smb2_test(suite, "streams-pwrite-hole", test_streams_pwrite_hole);
+
+       suite->description = talloc_strdup(suite, "vfs_streams_xattr tests");
+
+       return suite;
+}
index 3d402eeee0d235867fd9fb327c8887430548ab6a..19dbaa0775cc53e773cac90140e3eaf54402f24a 100644 (file)
@@ -115,6 +115,7 @@ NTSTATUS torture_vfs_init(TALLOC_CTX *ctx)
        torture_suite_add_suite(suite, torture_vfs_fruit_timemachine(suite));
        torture_suite_add_suite(suite, torture_vfs_fruit_conversion(suite));
        torture_suite_add_suite(suite, torture_vfs_fruit_unfruit(suite));
+       torture_suite_add_suite(suite, torture_vfs_streams_xattr(suite));
        torture_suite_add_1smb2_test(suite, "fruit_validate_afpinfo", test_fruit_validate_afpinfo);
 
        torture_register_suite(ctx, suite);
index b38a30c98da112a1bab35ffd6de3826fe04dea7e..cae558398a3267c6f208624a62d3f1a5335c4973 100644 (file)
@@ -301,7 +301,7 @@ bld.SAMBA_MODULE('TORTURE_NTP',
        )
 
 bld.SAMBA_MODULE('TORTURE_VFS',
-       source='vfs/vfs.c vfs/fruit.c vfs/acl_xattr.c',
+       source='vfs/vfs.c vfs/fruit.c vfs/acl_xattr.c vfs/streams_xattr.c',
        subsystem='smbtorture',
        deps='LIBCLI_SMB TORTURE_UTIL smbclient-raw TORTURE_RAW',
        internal_module=True,