]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
VFS: Add vfs_widelinks module.
authorJeremy Allison <jra@samba.org>
Sat, 4 Apr 2020 01:24:42 +0000 (18:24 -0700)
committerRalph Boehme <slow@samba.org>
Thu, 9 Apr 2020 19:40:34 +0000 (19:40 +0000)
Hides symlinks from smbd. Will be used to replace
the lp_widelinks() code inside smbd.

Long description of how this module works
with notes is included.

The man page and WHATSNEW.txt update is done
in a later patch in this series.

Signed-off-by: Jeremy Allison <jra@samba.org>
Reviewed-by: Ralph Boehme <slow@samba.org>
source3/modules/vfs_widelinks.c [new file with mode: 0644]
source3/modules/wscript_build
source3/wscript

diff --git a/source3/modules/vfs_widelinks.c b/source3/modules/vfs_widelinks.c
new file mode 100644 (file)
index 0000000..9a005c9
--- /dev/null
@@ -0,0 +1,545 @@
+/*
+ * Widelinks VFS module. Causes smbd not to see symlinks.
+ *
+ * Copyright (C) Jeremy Allison, 2020
+ *
+ * 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/>.
+ */
+
+/*
+ What does this module do ? It implements the explicitly insecure
+ "widelinks = yes" functionality that used to be in the core smbd
+ code.
+
+ Now this is implemented here, the insecure share-escape code that
+ explicitly allows escape from an exported share path can be removed
+ from smbd, leaving it a cleaner and more maintainable code base.
+
+ The smbd code can now always return ACCESS_DENIED if a path
+ leads outside a share.
+
+ How does it do that ? There are 2 features.
+
+ 1). When the upper layer code does a chdir() call to a pathname,
+ this module stores the requested pathname inside config->cwd.
+
+ When the upper layer code does a getwd() or reapath(), we return
+ the absolute path of the value stored in config->cwd, *not* the
+ position on the underlying filesystem.
+
+ This hides symlinks as if the chdir pathname contains a symlink,
+ normally doing a realpath call on it would return the real
+ position on the filesystem. For widelinks = yes, this isn't what
+ you want. You want the position you think is underneath the share
+ definition - the symlink path you used to go outside the share,
+ not the contents of the symlink itself.
+
+ That way, the upper layer smbd code can strictly enforce paths
+ being underneath a share definition without the knowledge that
+ "widelinks = yes" has moved us outside the share definition.
+
+ 1a). Note that when setting up a share, smbd may make calls such
+ as realpath and stat/lstat in order to set up the share definition.
+ These calls are made *before* smbd calls chdir() to move the working
+ directory below the exported share definition. In order to allow
+ this, all the vfs_widelinks functions are coded to just pass through
+ the vfs call to the next module in the chain if (a). The widelinks
+ module was loaded in error by an administrator and widelinks is
+ set to "no". This is the:
+
+       if (!config->active) {
+               Module not active.
+               SMB_VFS_NEXT_XXXXX(...)
+       }
+
+ idiom in the vfs functions.
+
+ 1b). If the module was correctly active, but smbd has yet
+ to call chdir(), then config->cwd == NULL. In that case
+ the correct action (to match the previous widelinks behavior
+ in the code inside smbd) is to pass through the vfs call to
+ the next module in the chain. That way, any symlinks in the
+ pathname are still exposed to smbd, which will restrict them to
+ be under the exported share definition. This allows the module
+ to "fail safe" for any vfs call made when setting up the share
+ structure definition, rather than fail unsafe by hiding symlinks
+ before chdir is called. This is the:
+
+       if (config->cwd == NULL) {
+               XXXXX syscall before chdir - see note 1b above.
+               return SMB_VFS_NEXT_XXXXX()
+       }
+
+ idiom in the vfs functions.
+
+ 2). The module hides the existance of symlinks by inside
+ lstat(), open(), and readdir() so long as it's not a POSIX
+ pathname request (those requests *must* be aware of symlinks
+ and the POSIX client has to follow them, it's expected that
+ a server will always fail to follow symlinks).
+
+ It does this by:
+
+ 2a). lstat -> stat
+ 2b). open removes any O_NOFOLLOW from flags.
+ 2c). The optimization in readdir that returns a stat
+ struct is removed as this could return a symlink mode
+ bit, causing smbd to always call stat/lstat itself on
+ a pathname (which we'll then use to hide symlinks).
+
+*/
+
+#include "includes.h"
+#include "smbd/smbd.h"
+
+struct widelinks_config {
+       bool active;
+       char *cwd;
+};
+
+/*
+ * Canonicalizes an absolute path, removing '.' and ".." components
+ * and consolitating multiple '/' characters. As this can only be
+ * called once widelinks_chdir with an absolute path has been called,
+ * we can assert that the start of path must be '/'.
+ */
+
+static char *resolve_realpath_name(TALLOC_CTX *ctx, const char *pathname_in)
+{
+       const char *s = pathname_in;
+       char *pathname = talloc_array(ctx, char, strlen(pathname_in)+1);
+       char *p = pathname;
+       bool wrote_slash = false;
+
+       if (pathname == NULL) {
+               return NULL;
+       }
+
+       SMB_ASSERT(pathname_in[0] == '/');
+
+       while (*s) {
+               /* Deal with '/' or multiples of '/'. */
+               if (s[0] == '/') {
+                       while (s[0] == '/') {
+                               /* Eat trailing '/' */
+                               s++;
+                       }
+                       /* Update target with one '/' */
+                       if (!wrote_slash) {
+                               *p++ = '/';
+                               wrote_slash = true;
+                       }
+                       continue;
+               }
+               if (wrote_slash) {
+                       /* Deal with "./" or ".\0" */
+                       if (s[0] == '.' &&
+                                       (s[1] == '/' || s[1] == '\0')) {
+                               /* Eat the dot. */
+                               s++;
+                               while (s[0] == '/') {
+                                       /* Eat any trailing '/' */
+                                       s++;
+                               }
+                               /* Don't write anything to target. */
+                               /* wrote_slash is still true. */
+                               continue;
+                       }
+                       /* Deal with "../" or "..\0" */
+                       if (s[0] == '.' && s[1] == '.' &&
+                                       (s[2] == '/' || s[2] == '\0')) {
+                               /* Eat the dot dot. */
+                               s += 2;
+                               while (s[0] == '/') {
+                                       /* Eat any trailing '/' */
+                                       s++;
+                               }
+                               /*
+                                * As wrote_slash is true, we go back
+                                * one character to point p at the slash
+                                * we just saw.
+                                */
+                               if (p > pathname) {
+                                       p--;
+                               }
+                               /*
+                                * Now go back to the slash
+                                * before the one that p currently points to.
+                                */
+                               while (p > pathname) {
+                                       p--;
+                                       if (p[0] == '/') {
+                                               break;
+                                       }
+                               }
+                               /*
+                                * Step forward one to leave the
+                                * last written '/' alone.
+                                */
+                               p++;
+
+                               /* Don't write anything to target. */
+                               /* wrote_slash is still true. */
+                               continue;
+                       }
+               }
+               /* Non-separator character, just copy. */
+               *p++ = *s++;
+               wrote_slash = false;
+       }
+       if (wrote_slash) {
+               /*
+                * We finished on a '/'.
+                * Remove the trailing '/', but not if it's
+                * the sole character in the path.
+                */
+               if (p > pathname + 1) {
+                       p--;
+               }
+       }
+       /* Terminate and we're done ! */
+       *p++ = '\0';
+       return pathname;
+}
+
+static int widelinks_connect(struct vfs_handle_struct *handle,
+                       const char *service,
+                       const char *user)
+{
+       struct widelinks_config *config;
+       int ret;
+
+       ret = SMB_VFS_NEXT_CONNECT(handle,
+                               service,
+                               user);
+       if (ret != 0) {
+               return ret;
+       }
+
+       config = talloc_zero(handle->conn,
+                               struct widelinks_config);
+       if (!config) {
+               SMB_VFS_NEXT_DISCONNECT(handle);
+               return -1;
+       }
+       config->active = lp_widelinks(SNUM(handle->conn));
+       if (!config->active) {
+               DBG_ERR("vfs_widelinks module loaded with "
+                       "widelinks = no\n");
+       }
+
+        SMB_VFS_HANDLE_SET_DATA(handle,
+                               config,
+                               NULL, /* free_fn */
+                               struct widelinks_config,
+                               return -1);
+       return 0;
+}
+
+static int widelinks_chdir(struct vfs_handle_struct *handle,
+                               const struct smb_filename *smb_fname)
+{
+       int ret = -1;
+       struct widelinks_config *config = NULL;
+       char *new_cwd = NULL;
+
+       SMB_VFS_HANDLE_GET_DATA(handle,
+                               config,
+                               struct widelinks_config,
+                               return -1);
+
+       if (!config->active) {
+               /* Module not active. */
+               return SMB_VFS_NEXT_CHDIR(handle, smb_fname);
+       }
+
+       /*
+        * We know we never get a path continaing
+        * DOT or DOTDOT.
+        */
+
+       if (smb_fname->base_name[0] == '/') {
+               /* Absolute path - replace. */
+               new_cwd = talloc_strdup(config,
+                               smb_fname->base_name);
+       } else {
+               if (config->cwd == NULL) {
+                       /*
+                        * Relative chdir before absolute one -
+                        * see note 1b above.
+                        */
+                       struct smb_filename *current_dir_fname =
+                                       SMB_VFS_NEXT_GETWD(handle,
+                                                       config);
+                       if (current_dir_fname == NULL) {
+                               return -1;
+                       }
+                       /* Paranoia.. */
+                       if (current_dir_fname->base_name[0] != '/') {
+                               DBG_ERR("SMB_VFS_NEXT_GETWD returned "
+                                       "non-absolute path |%s|\n",
+                                       current_dir_fname->base_name);
+                               TALLOC_FREE(current_dir_fname);
+                               return -1;
+                       }
+                       config->cwd = talloc_strdup(config,
+                                       current_dir_fname->base_name);
+                       TALLOC_FREE(current_dir_fname);
+                       if (config->cwd == NULL) {
+                               return -1;
+                       }
+               }
+               new_cwd = talloc_asprintf(config,
+                               "%s/%s",
+                               config->cwd,
+                               smb_fname->base_name);
+       }
+       if (new_cwd == NULL) {
+               return -1;
+       }
+       ret = SMB_VFS_NEXT_CHDIR(handle, smb_fname);
+       if (ret == -1) {
+               TALLOC_FREE(new_cwd);
+               return ret;
+       }
+       /* Replace the cache we use for realpath/getwd. */
+       TALLOC_FREE(config->cwd);
+       config->cwd = new_cwd;
+       DBG_DEBUG("config->cwd now |%s|\n", config->cwd);
+       return 0;
+}
+
+static struct smb_filename *widelinks_getwd(vfs_handle_struct *handle,
+                                TALLOC_CTX *ctx)
+{
+       struct widelinks_config *config = NULL;
+
+       SMB_VFS_HANDLE_GET_DATA(handle,
+                               config,
+                               struct widelinks_config,
+                               return NULL);
+
+       if (!config->active) {
+               /* Module not active. */
+               return SMB_VFS_NEXT_GETWD(handle, ctx);
+       }
+       if (config->cwd == NULL) {
+               /* getwd before chdir. See note 1b above. */
+               return SMB_VFS_NEXT_GETWD(handle, ctx);
+       }
+       return synthetic_smb_fname(ctx,
+                               config->cwd,
+                               NULL,
+                               NULL,
+                               0);
+}
+
+static struct smb_filename *widelinks_realpath(vfs_handle_struct *handle,
+                       TALLOC_CTX *ctx,
+                       const struct smb_filename *smb_fname_in)
+{
+       struct widelinks_config *config = NULL;
+       char *pathname = NULL;
+       char *resolved_pathname = NULL;
+       struct smb_filename *smb_fname;
+
+       SMB_VFS_HANDLE_GET_DATA(handle,
+                               config,
+                               struct widelinks_config,
+                               return NULL);
+
+       if (!config->active) {
+               /* Module not active. */
+               return SMB_VFS_NEXT_REALPATH(handle,
+                               ctx,
+                               smb_fname_in);
+       }
+
+       if (config->cwd == NULL) {
+               /* realpath before chdir. See note 1b above. */
+               return SMB_VFS_NEXT_REALPATH(handle,
+                               ctx,
+                               smb_fname_in);
+       }
+
+       if (smb_fname_in->base_name[0] == '/') {
+               /* Absolute path - process as-is. */
+               pathname = talloc_strdup(config,
+                                       smb_fname_in->base_name);
+       } else {
+               /* Relative path - most commonly "." */
+               pathname = talloc_asprintf(config,
+                               "%s/%s",
+                               config->cwd,
+                               smb_fname_in->base_name);
+       }
+       resolved_pathname = resolve_realpath_name(config, pathname);
+       if (resolved_pathname == NULL) {
+               TALLOC_FREE(pathname);
+               return NULL;
+       }
+
+       DBG_DEBUG("realpath |%s| -> |%s| -> |%s|\n",
+                       smb_fname_in->base_name,
+                       pathname,
+                       resolved_pathname);
+
+       smb_fname = synthetic_smb_fname(ctx,
+                               resolved_pathname,
+                               NULL,
+                               NULL,
+                               0);
+       TALLOC_FREE(pathname);
+       TALLOC_FREE(resolved_pathname);
+       return smb_fname;
+}
+
+static int widelinks_lstat(vfs_handle_struct *handle,
+                       struct smb_filename *smb_fname)
+{
+       struct widelinks_config *config = NULL;
+
+       SMB_VFS_HANDLE_GET_DATA(handle,
+                               config,
+                               struct widelinks_config,
+                               return -1);
+
+       if (!config->active) {
+               /* Module not active. */
+               return SMB_VFS_NEXT_LSTAT(handle,
+                               smb_fname);
+       }
+
+       if (config->cwd == NULL) {
+               /* lstat before chdir. See note 1b above. */
+               return SMB_VFS_NEXT_LSTAT(handle,
+                               smb_fname);
+       }
+
+       if (smb_fname->flags & SMB_FILENAME_POSIX_PATH) {
+               /* POSIX sees symlinks. */
+               return SMB_VFS_NEXT_LSTAT(handle,
+                               smb_fname);
+       }
+
+       /* Replace with STAT. */
+       return SMB_VFS_NEXT_STAT(handle, smb_fname);
+}
+
+static int widelinks_open(vfs_handle_struct *handle,
+                       struct smb_filename *smb_fname,
+                       files_struct *fsp,
+                       int flags,
+                       mode_t mode)
+{
+       struct widelinks_config *config = NULL;
+
+       SMB_VFS_HANDLE_GET_DATA(handle,
+                               config,
+                               struct widelinks_config,
+                               return -1);
+
+       if (!config->active) {
+               /* Module not active. */
+               return SMB_VFS_NEXT_OPEN(handle,
+                               smb_fname,
+                               fsp,
+                               flags,
+                               mode);
+       }
+
+       if (config->cwd == NULL) {
+               /* open before chdir. See note 1b above. */
+               return SMB_VFS_NEXT_OPEN(handle,
+                               smb_fname,
+                               fsp,
+                               flags,
+                               mode);
+       }
+
+       if (smb_fname->flags & SMB_FILENAME_POSIX_PATH) {
+               /* POSIX sees symlinks. */
+               return SMB_VFS_NEXT_OPEN(handle,
+                               smb_fname,
+                               fsp,
+                               flags,
+                               mode);
+       }
+
+       /* Remove O_NOFOLLOW. */
+       flags = (flags & ~O_NOFOLLOW);
+
+       return SMB_VFS_NEXT_OPEN(handle,
+                       smb_fname,
+                       fsp,
+                       flags,
+                       mode);
+}
+
+static struct dirent *widelinks_readdir(vfs_handle_struct *handle,
+                       DIR *dirp,
+                       SMB_STRUCT_STAT *sbuf)
+{
+       struct widelinks_config *config = NULL;
+       struct dirent *result;
+
+       SMB_VFS_HANDLE_GET_DATA(handle,
+                               config,
+                               struct widelinks_config,
+                               return NULL);
+
+       result = SMB_VFS_NEXT_READDIR(handle,
+                               dirp,
+                               sbuf);
+
+       if (!config->active) {
+               /* Module not active. */
+               return result;
+       }
+
+       /*
+        * Prevent optimization of returning
+        * the stat info. Force caller to go
+        * through our LSTAT that hides symlinks.
+        */
+
+       if (sbuf) {
+               SET_STAT_INVALID(*sbuf);
+       }
+       return result;
+}
+
+static struct vfs_fn_pointers vfs_widelinks_fns = {
+       .connect_fn = widelinks_connect,
+
+       .open_fn = widelinks_open,
+       .lstat_fn = widelinks_lstat,
+       /*
+        * NB. We don't need an lchown function as this
+        * is only called (a) on directory create and
+        * (b) on POSIX extensions names.
+        */
+       .chdir_fn = widelinks_chdir,
+       .getwd_fn = widelinks_getwd,
+       .realpath_fn = widelinks_realpath,
+       .readdir_fn = widelinks_readdir
+};
+
+static_decl_vfs;
+NTSTATUS vfs_widelinks_init(TALLOC_CTX *ctx)
+{
+       return smb_register_vfs(SMB_VFS_INTERFACE_VERSION,
+                               "widelinks",
+                               &vfs_widelinks_fns);
+}
index 41d8568e43a627b595481f7c9365505ff47f2183..57a1bfb59feab064e15fe557de6907cbbf5323fe 100644 (file)
@@ -616,3 +616,10 @@ bld.SAMBA3_MODULE('vfs_delay_inject',
                  init_function='',
                  internal_module=bld.SAMBA3_IS_STATIC_MODULE('vfs_delay_inject'),
                  enabled=bld.SAMBA3_IS_ENABLED_MODULE('vfs_delay_inject'))
+
+bld.SAMBA3_MODULE('vfs_widelinks',
+                 subsystem='vfs',
+                 source='vfs_widelinks.c',
+                 init_function='',
+                 internal_module=bld.SAMBA3_IS_STATIC_MODULE('vfs_widelinks'),
+                 enabled=bld.SAMBA3_IS_ENABLED_MODULE('vfs_widelinks'))
index 48194f261a487a2363838d6ce4c05398ba0b3588..76abcf79c9053c6b2be78372ee0785be837b0d21 100644 (file)
@@ -1925,7 +1925,7 @@ main() {
                                       vfs_preopen vfs_catia
                                       vfs_media_harmony vfs_unityed_media vfs_fruit vfs_shell_snap
                                       vfs_commit vfs_worm vfs_crossrename vfs_linux_xfs_sgid
-                                      vfs_time_audit vfs_offline vfs_virusfilter
+                                      vfs_time_audit vfs_offline vfs_virusfilter vfs_widelinks
                                   '''))
     default_shared_modules.extend(TO_LIST('idmap_tdb2 idmap_script'))
     # these have broken dependencies