LCOV - code coverage report
Current view: top level - source3/modules - vfs_widelinks.c (source / functions) Hit Total Coverage
Test: coverage report for v4-17-test 1498b464 Lines: 62 128 48.4 %
Date: 2024-06-13 04:01:37 Functions: 6 9 66.7 %

          Line data    Source code
       1             : /*
       2             :  * Widelinks VFS module. Causes smbd not to see symlinks.
       3             :  *
       4             :  * Copyright (C) Jeremy Allison, 2020
       5             :  *
       6             :  * This program is free software; you can redistribute it and/or modify
       7             :  * it under the terms of the GNU General Public License as published by
       8             :  * the Free Software Foundation; either version 3 of the License, or
       9             :  * (at your option) any later version.
      10             :  *
      11             :  * This program is distributed in the hope that it will be useful,
      12             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      13             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      14             :  * GNU General Public License for more details.
      15             :  *
      16             :  * You should have received a copy of the GNU General Public License
      17             :  * along with this program; if not, see <http://www.gnu.org/licenses/>.
      18             :  */
      19             : 
      20             : /*
      21             :  What does this module do ? It implements the explicitly insecure
      22             :  "widelinks = yes" functionality that used to be in the core smbd
      23             :  code.
      24             : 
      25             :  Now this is implemented here, the insecure share-escape code that
      26             :  explicitly allows escape from an exported share path can be removed
      27             :  from smbd, leaving it a cleaner and more maintainable code base.
      28             : 
      29             :  The smbd code can now always return ACCESS_DENIED if a path
      30             :  leads outside a share.
      31             : 
      32             :  How does it do that ? There are 2 features.
      33             : 
      34             :  1). When the upper layer code does a chdir() call to a pathname,
      35             :  this module stores the requested pathname inside config->cwd.
      36             : 
      37             :  When the upper layer code does a getwd() or realpath(), we return
      38             :  the absolute path of the value stored in config->cwd, *not* the
      39             :  position on the underlying filesystem.
      40             : 
      41             :  This hides symlinks as if the chdir pathname contains a symlink,
      42             :  normally doing a realpath call on it would return the real
      43             :  position on the filesystem. For widelinks = yes, this isn't what
      44             :  you want. You want the position you think is underneath the share
      45             :  definition - the symlink path you used to go outside the share,
      46             :  not the contents of the symlink itself.
      47             : 
      48             :  That way, the upper layer smbd code can strictly enforce paths
      49             :  being underneath a share definition without the knowledge that
      50             :  "widelinks = yes" has moved us outside the share definition.
      51             : 
      52             :  1a). Note that when setting up a share, smbd may make calls such
      53             :  as realpath and stat/lstat in order to set up the share definition.
      54             :  These calls are made *before* smbd calls chdir() to move the working
      55             :  directory below the exported share definition. In order to allow
      56             :  this, all the vfs_widelinks functions are coded to just pass through
      57             :  the vfs call to the next module in the chain if (a). The widelinks
      58             :  module was loaded in error by an administrator and widelinks is
      59             :  set to "no". This is the:
      60             : 
      61             :         if (!config->active) {
      62             :                 Module not active.
      63             :                 SMB_VFS_NEXT_XXXXX(...)
      64             :         }
      65             : 
      66             :  idiom in the vfs functions.
      67             : 
      68             :  1b). If the module was correctly active, but smbd has yet
      69             :  to call chdir(), then config->cwd == NULL. In that case
      70             :  the correct action (to match the previous widelinks behavior
      71             :  in the code inside smbd) is to pass through the vfs call to
      72             :  the next module in the chain. That way, any symlinks in the
      73             :  pathname are still exposed to smbd, which will restrict them to
      74             :  be under the exported share definition. This allows the module
      75             :  to "fail safe" for any vfs call made when setting up the share
      76             :  structure definition, rather than fail unsafe by hiding symlinks
      77             :  before chdir is called. This is the:
      78             : 
      79             :         if (config->cwd == NULL) {
      80             :                 XXXXX syscall before chdir - see note 1b above.
      81             :                 return SMB_VFS_NEXT_XXXXX()
      82             :         }
      83             : 
      84             :  idiom in the vfs functions.
      85             : 
      86             :  2). The module hides the existance of symlinks by inside
      87             :  lstat(), open(), and readdir() so long as it's not a POSIX
      88             :  pathname request (those requests *must* be aware of symlinks
      89             :  and the POSIX client has to follow them, it's expected that
      90             :  a server will always fail to follow symlinks).
      91             : 
      92             :  It does this by:
      93             : 
      94             :  2a). lstat -> stat
      95             :  2b). open removes any O_NOFOLLOW from flags.
      96             :  2c). The optimization in readdir that returns a stat
      97             :  struct is removed as this could return a symlink mode
      98             :  bit, causing smbd to always call stat/lstat itself on
      99             :  a pathname (which we'll then use to hide symlinks).
     100             : 
     101             : */
     102             : 
     103             : #include "includes.h"
     104             : #include "smbd/smbd.h"
     105             : #include "lib/util_path.h"
     106             : 
     107             : struct widelinks_config {
     108             :         bool active;
     109             :         bool is_dfs_share;
     110             :         char *cwd;
     111             : };
     112             : 
     113           8 : static int widelinks_connect(struct vfs_handle_struct *handle,
     114             :                         const char *service,
     115             :                         const char *user)
     116             : {
     117             :         struct widelinks_config *config;
     118             :         int ret;
     119             : 
     120           8 :         ret = SMB_VFS_NEXT_CONNECT(handle,
     121             :                                 service,
     122             :                                 user);
     123           8 :         if (ret != 0) {
     124           0 :                 return ret;
     125             :         }
     126             : 
     127           8 :         config = talloc_zero(handle->conn,
     128             :                                 struct widelinks_config);
     129           8 :         if (!config) {
     130           0 :                 SMB_VFS_NEXT_DISCONNECT(handle);
     131           0 :                 return -1;
     132             :         }
     133           8 :         config->active = lp_widelinks(SNUM(handle->conn));
     134           8 :         if (!config->active) {
     135           0 :                 DBG_ERR("vfs_widelinks module loaded with "
     136             :                         "widelinks = no\n");
     137             :         }
     138           8 :         config->is_dfs_share =
     139           8 :                 (lp_host_msdfs() && lp_msdfs_root(SNUM(handle->conn)));
     140           8 :         SMB_VFS_HANDLE_SET_DATA(handle,
     141             :                                 config,
     142             :                                 NULL, /* free_fn */
     143             :                                 struct widelinks_config,
     144             :                                 return -1);
     145           8 :         return 0;
     146             : }
     147             : 
     148          56 : static int widelinks_chdir(struct vfs_handle_struct *handle,
     149             :                                 const struct smb_filename *smb_fname)
     150             : {
     151          56 :         int ret = -1;
     152          56 :         struct widelinks_config *config = NULL;
     153          56 :         char *new_cwd = NULL;
     154             : 
     155          56 :         SMB_VFS_HANDLE_GET_DATA(handle,
     156             :                                 config,
     157             :                                 struct widelinks_config,
     158             :                                 return -1);
     159             : 
     160          56 :         if (!config->active) {
     161             :                 /* Module not active. */
     162           0 :                 return SMB_VFS_NEXT_CHDIR(handle, smb_fname);
     163             :         }
     164             : 
     165             :         /*
     166             :          * We know we never get a path containing
     167             :          * DOT or DOTDOT.
     168             :          */
     169             : 
     170          56 :         if (smb_fname->base_name[0] == '/') {
     171             :                 /* Absolute path - replace. */
     172          48 :                 new_cwd = talloc_strdup(config,
     173          48 :                                 smb_fname->base_name);
     174             :         } else {
     175           8 :                 if (config->cwd == NULL) {
     176             :                         /*
     177             :                          * Relative chdir before absolute one -
     178             :                          * see note 1b above.
     179             :                          */
     180           0 :                         struct smb_filename *current_dir_fname =
     181           0 :                                         SMB_VFS_NEXT_GETWD(handle,
     182             :                                                         config);
     183           0 :                         if (current_dir_fname == NULL) {
     184           0 :                                 return -1;
     185             :                         }
     186             :                         /* Paranoia.. */
     187           0 :                         if (current_dir_fname->base_name[0] != '/') {
     188           0 :                                 DBG_ERR("SMB_VFS_NEXT_GETWD returned "
     189             :                                         "non-absolute path |%s|\n",
     190             :                                         current_dir_fname->base_name);
     191           0 :                                 TALLOC_FREE(current_dir_fname);
     192           0 :                                 return -1;
     193             :                         }
     194           0 :                         config->cwd = talloc_strdup(config,
     195           0 :                                         current_dir_fname->base_name);
     196           0 :                         TALLOC_FREE(current_dir_fname);
     197           0 :                         if (config->cwd == NULL) {
     198           0 :                                 return -1;
     199             :                         }
     200             :                 }
     201           8 :                 new_cwd = talloc_asprintf(config,
     202             :                                 "%s/%s",
     203             :                                 config->cwd,
     204           4 :                                 smb_fname->base_name);
     205             :         }
     206          56 :         if (new_cwd == NULL) {
     207           0 :                 return -1;
     208             :         }
     209          56 :         ret = SMB_VFS_NEXT_CHDIR(handle, smb_fname);
     210          56 :         if (ret == -1) {
     211          40 :                 TALLOC_FREE(new_cwd);
     212          40 :                 return ret;
     213             :         }
     214             :         /* Replace the cache we use for realpath/getwd. */
     215          16 :         TALLOC_FREE(config->cwd);
     216          16 :         config->cwd = new_cwd;
     217          16 :         DBG_DEBUG("config->cwd now |%s|\n", config->cwd);
     218          16 :         return 0;
     219             : }
     220             : 
     221           0 : static struct smb_filename *widelinks_getwd(vfs_handle_struct *handle,
     222             :                                 TALLOC_CTX *ctx)
     223             : {
     224           0 :         struct widelinks_config *config = NULL;
     225             : 
     226           0 :         SMB_VFS_HANDLE_GET_DATA(handle,
     227             :                                 config,
     228             :                                 struct widelinks_config,
     229             :                                 return NULL);
     230             : 
     231           0 :         if (!config->active) {
     232             :                 /* Module not active. */
     233           0 :                 return SMB_VFS_NEXT_GETWD(handle, ctx);
     234             :         }
     235           0 :         if (config->cwd == NULL) {
     236             :                 /* getwd before chdir. See note 1b above. */
     237           0 :                 return SMB_VFS_NEXT_GETWD(handle, ctx);
     238             :         }
     239           0 :         return synthetic_smb_fname(ctx,
     240           0 :                                 config->cwd,
     241             :                                 NULL,
     242             :                                 NULL,
     243             :                                 0,
     244             :                                 0);
     245             : }
     246             : 
     247          24 : static struct smb_filename *widelinks_realpath(vfs_handle_struct *handle,
     248             :                         TALLOC_CTX *ctx,
     249             :                         const struct smb_filename *smb_fname_in)
     250             : {
     251          24 :         struct widelinks_config *config = NULL;
     252          24 :         char *pathname = NULL;
     253          24 :         char *resolved_pathname = NULL;
     254             :         struct smb_filename *smb_fname;
     255             : 
     256          24 :         SMB_VFS_HANDLE_GET_DATA(handle,
     257             :                                 config,
     258             :                                 struct widelinks_config,
     259             :                                 return NULL);
     260             : 
     261          24 :         if (!config->active) {
     262             :                 /* Module not active. */
     263           0 :                 return SMB_VFS_NEXT_REALPATH(handle,
     264             :                                 ctx,
     265             :                                 smb_fname_in);
     266             :         }
     267             : 
     268          24 :         if (config->cwd == NULL) {
     269             :                 /* realpath before chdir. See note 1b above. */
     270          16 :                 return SMB_VFS_NEXT_REALPATH(handle,
     271             :                                 ctx,
     272             :                                 smb_fname_in);
     273             :         }
     274             : 
     275           8 :         if (smb_fname_in->base_name[0] == '/') {
     276             :                 /* Absolute path - process as-is. */
     277           0 :                 pathname = talloc_strdup(config,
     278           0 :                                         smb_fname_in->base_name);
     279             :         } else {
     280             :                 /* Relative path - most commonly "." */
     281           8 :                 pathname = talloc_asprintf(config,
     282             :                                 "%s/%s",
     283             :                                 config->cwd,
     284           4 :                                 smb_fname_in->base_name);
     285             :         }
     286             : 
     287           8 :         SMB_ASSERT(pathname[0] == '/');
     288             : 
     289           8 :         resolved_pathname = canonicalize_absolute_path(config, pathname);
     290           8 :         if (resolved_pathname == NULL) {
     291           0 :                 TALLOC_FREE(pathname);
     292           0 :                 return NULL;
     293             :         }
     294             : 
     295           8 :         DBG_DEBUG("realpath |%s| -> |%s| -> |%s|\n",
     296             :                         smb_fname_in->base_name,
     297             :                         pathname,
     298             :                         resolved_pathname);
     299             : 
     300           8 :         smb_fname = synthetic_smb_fname(ctx,
     301             :                                 resolved_pathname,
     302             :                                 NULL,
     303             :                                 NULL,
     304             :                                 0,
     305             :                                 0);
     306           8 :         TALLOC_FREE(pathname);
     307           8 :         TALLOC_FREE(resolved_pathname);
     308           8 :         return smb_fname;
     309             : }
     310             : 
     311           0 : static int widelinks_lstat(vfs_handle_struct *handle,
     312             :                         struct smb_filename *smb_fname)
     313             : {
     314           0 :         struct widelinks_config *config = NULL;
     315             : 
     316           0 :         SMB_VFS_HANDLE_GET_DATA(handle,
     317             :                                 config,
     318             :                                 struct widelinks_config,
     319             :                                 return -1);
     320             : 
     321           0 :         if (!config->active) {
     322             :                 /* Module not active. */
     323           0 :                 return SMB_VFS_NEXT_LSTAT(handle,
     324             :                                 smb_fname);
     325             :         }
     326             : 
     327           0 :         if (config->cwd == NULL) {
     328             :                 /* lstat before chdir. See note 1b above. */
     329           0 :                 return SMB_VFS_NEXT_LSTAT(handle,
     330             :                                 smb_fname);
     331             :         }
     332             : 
     333           0 :         if (smb_fname->flags & SMB_FILENAME_POSIX_PATH) {
     334             :                 /* POSIX sees symlinks. */
     335           0 :                 return SMB_VFS_NEXT_LSTAT(handle,
     336             :                                 smb_fname);
     337             :         }
     338             : 
     339             :         /* Replace with STAT. */
     340           0 :         return SMB_VFS_NEXT_STAT(handle, smb_fname);
     341             : }
     342             : 
     343          16 : static int widelinks_openat(vfs_handle_struct *handle,
     344             :                             const struct files_struct *dirfsp,
     345             :                             const struct smb_filename *smb_fname,
     346             :                             files_struct *fsp,
     347             :                             const struct vfs_open_how *_how)
     348             : {
     349          16 :         struct vfs_open_how how = *_how;
     350          16 :         struct widelinks_config *config = NULL;
     351             :         int ret;
     352          16 :         SMB_VFS_HANDLE_GET_DATA(handle,
     353             :                                 config,
     354             :                                 struct widelinks_config,
     355             :                                 return -1);
     356             : 
     357          24 :         if (config->active &&
     358          16 :             (config->cwd != NULL) &&
     359           0 :             !(smb_fname->flags & SMB_FILENAME_POSIX_PATH))
     360             :         {
     361             :                 /*
     362             :                  * Module active, openat after chdir (see note 1b above) and not
     363             :                  * a POSIX open (POSIX sees symlinks), so remove O_NOFOLLOW.
     364             :                  */
     365           0 :                 how.flags = (how.flags & ~O_NOFOLLOW);
     366             :         }
     367             : 
     368          16 :         ret = SMB_VFS_NEXT_OPENAT(handle,
     369             :                                    dirfsp,
     370             :                                    smb_fname,
     371             :                                    fsp,
     372             :                                    &how);
     373          16 :         if (config->is_dfs_share && ret == -1 && errno == ENOENT) {
     374           0 :                 struct smb_filename *full_fname = NULL;
     375             :                 int lstat_ret;
     376             : 
     377           0 :                 full_fname = full_path_from_dirfsp_atname(talloc_tos(),
     378             :                                 dirfsp,
     379             :                                 smb_fname);
     380           0 :                 if (full_fname == NULL) {
     381           0 :                         errno = ENOMEM;
     382           0 :                         return -1;
     383             :                 }
     384           0 :                 lstat_ret = SMB_VFS_NEXT_LSTAT(handle,
     385             :                                 full_fname);
     386           0 :                 if (lstat_ret != -1 &&
     387           0 :                     VALID_STAT(full_fname->st) &&
     388           0 :                     S_ISLNK(full_fname->st.st_ex_mode)) {
     389           0 :                         fsp->fsp_name->st = full_fname->st;
     390             :                 }
     391           0 :                 TALLOC_FREE(full_fname);
     392           0 :                 errno = ENOENT;
     393             :         }
     394          16 :         return ret;
     395             : }
     396             : 
     397           0 : static struct dirent *widelinks_readdir(vfs_handle_struct *handle,
     398             :                                         struct files_struct *dirfsp,
     399             :                                         DIR *dirp,
     400             :                                         SMB_STRUCT_STAT *sbuf)
     401             : {
     402           0 :         struct widelinks_config *config = NULL;
     403             :         struct dirent *result;
     404             : 
     405           0 :         SMB_VFS_HANDLE_GET_DATA(handle,
     406             :                                 config,
     407             :                                 struct widelinks_config,
     408             :                                 return NULL);
     409             : 
     410           0 :         result = SMB_VFS_NEXT_READDIR(handle,
     411             :                                       dirfsp,
     412             :                                       dirp,
     413             :                                       sbuf);
     414             : 
     415           0 :         if (!config->active) {
     416             :                 /* Module not active. */
     417           0 :                 return result;
     418             :         }
     419             : 
     420             :         /*
     421             :          * Prevent optimization of returning
     422             :          * the stat info. Force caller to go
     423             :          * through our LSTAT that hides symlinks.
     424             :          */
     425             : 
     426           0 :         if (sbuf) {
     427           0 :                 SET_STAT_INVALID(*sbuf);
     428             :         }
     429           0 :         return result;
     430             : }
     431             : 
     432             : static struct vfs_fn_pointers vfs_widelinks_fns = {
     433             :         .connect_fn = widelinks_connect,
     434             : 
     435             :         .openat_fn = widelinks_openat,
     436             :         .lstat_fn = widelinks_lstat,
     437             :         /*
     438             :          * NB. We don't need an lchown function as this
     439             :          * is only called (a) on directory create and
     440             :          * (b) on POSIX extensions names.
     441             :          */
     442             :         .chdir_fn = widelinks_chdir,
     443             :         .getwd_fn = widelinks_getwd,
     444             :         .realpath_fn = widelinks_realpath,
     445             :         .readdir_fn = widelinks_readdir
     446             : };
     447             : 
     448             : static_decl_vfs;
     449          34 : NTSTATUS vfs_widelinks_init(TALLOC_CTX *ctx)
     450             : {
     451          34 :         return smb_register_vfs(SMB_VFS_INTERFACE_VERSION,
     452             :                                 "widelinks",
     453             :                                 &vfs_widelinks_fns);
     454             : }

Generated by: LCOV version 1.13