Print this page
NEX-13644 File access audit logging
Reviewed by: Gordon Ross <gordon.ross@nexenta.com>
Reviewed by: Roman Strashkin <roman.strashkin@nexenta.com>
Reviewed by: Saso Kiselkov <saso.kiselkov@nexenta.com>
Reviewed by: Rick McNeal <rick.mcneal@nexenta.com>
Reviewed by: Yuri Pankov <yuri.pankov@nexenta.com>
NEX-5665 SMB2 oplock leases
Reviewed by: Matt Barden <matt.barden@nexenta.com>
Reviewed by: Evan Layton <evan.layton@nexenta.com>
Reviewed by: Roman Strashkin <roman.strashkin@nexenta.com>
NEX-5665 SMB2 oplock leases
Reviewed by: Matt Barden <matt.barden@nexenta.com>
Reviewed by: Evan Layton <evan.layton@nexenta.com>
Reviewed by: Roman Strashkin <roman.strashkin@nexenta.com>
NEX-2831 panic in smb_make_link
NEX-2442 regression with smbtorture test raw.sfileinfo.rename
NEX-1920 SMB rename from Win2k8 fails
SMB-11 SMB2 message parse & dispatch
SMB-12 SMB2 Negotiate Protocol
SMB-13 SMB2 Session Setup
SMB-14 SMB2 Logoff
SMB-15 SMB2 Tree Connect
SMB-16 SMB2 Tree Disconnect
SMB-17 SMB2 Create
SMB-18 SMB2 Close
SMB-19 SMB2 Flush
SMB-20 SMB2 Read
SMB-21 SMB2 Write
SMB-22 SMB2 Lock/Unlock
SMB-23 SMB2 Ioctl
SMB-24 SMB2 Cancel
SMB-25 SMB2 Echo
SMB-26 SMB2 Query Dir
SMB-27 SMB2 Change Notify
SMB-28 SMB2 Query Info
SMB-29 SMB2 Set Info
SMB-30 SMB2 Oplocks
SMB-53 SMB2 Create Context options
(SMB2 code review cleanup 1, 2, 3)

Split Close
Expand all
Collapse all
          --- old/usr/src/uts/common/fs/smbsrv/smb_cmn_rename.c
          +++ new/usr/src/uts/common/fs/smbsrv/smb_cmn_rename.c
↓ open down ↓ 12 lines elided ↑ open up ↑
  13   13   * When distributing Covered Code, include this CDDL HEADER in each
  14   14   * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
  15   15   * If applicable, add the following below this CDDL HEADER, with the
  16   16   * fields enclosed by brackets "[]" replaced with your own identifying
  17   17   * information: Portions Copyright [yyyy] [name of copyright owner]
  18   18   *
  19   19   * CDDL HEADER END
  20   20   */
  21   21  /*
  22   22   * Copyright (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved.
  23      - * Copyright 2014 Nexenta Systems, Inc.  All rights reserved.
       23 + * Copyright 2018 Nexenta Systems, Inc.  All rights reserved.
  24   24   */
  25   25  
  26   26  #include <sys/synch.h>
  27      -#include <smbsrv/smb_kproto.h>
       27 +#include <smbsrv/smb2_kproto.h>
  28   28  #include <smbsrv/smb_fsops.h>
  29   29  #include <sys/nbmlock.h>
  30   30  
  31   31  /*
  32   32   * SMB_TRANS2_SET_FILE/PATH_INFO (RENAME_INFORMATION level) flag
  33   33   */
  34   34  #define SMB_RENAME_FLAG_OVERWRITE       0x001
  35   35  
  36   36  static int smb_rename_check_stream(smb_fqi_t *, smb_fqi_t *);
  37   37  static int smb_rename_check_attr(smb_request_t *, smb_node_t *, uint16_t);
  38   38  static int smb_rename_lookup_src(smb_request_t *);
       39 +static uint32_t smb_rename_check_src(smb_request_t *, smb_fqi_t *);
  39   40  static void smb_rename_release_src(smb_request_t *);
  40   41  static uint32_t smb_rename_errno2status(int);
  41   42  
  42   43  /*
  43   44   * smb_setinfo_rename
  44   45   *
  45   46   * Implements SMB_FILE_RENAME_INFORMATION level of Trans2_Set_FileInfo
  46   47   * and Trans2_Set_PathInfo and SMB2 set_info, FileRenameInformation.
  47   48   * If the new filename (dst_fqi) already exists it may be overwritten
  48   49   * if flags == 1.
↓ open down ↓ 43 lines elided ↑ open up ↑
  92   93   * Similar to smb_make_link(), below.
  93   94   */
  94   95  uint32_t
  95   96  smb_common_rename(smb_request_t *sr, smb_fqi_t *src_fqi, smb_fqi_t *dst_fqi)
  96   97  {
  97   98          smb_node_t *src_fnode, *src_dnode, *dst_dnode;
  98   99          smb_node_t *dst_fnode = 0;
  99  100          smb_node_t *tnode;
 100  101          char *new_name, *path;
 101  102          DWORD status;
 102      -        int rc, count;
      103 +        int rc;
      104 +        boolean_t have_src = B_FALSE;
      105 +        boolean_t dst_exists = B_FALSE;
      106 +        boolean_t do_audit;
      107 +        char *srcpath = NULL;
      108 +        char *dstpath = NULL;
 103  109  
 104  110          tnode = sr->tid_tree->t_snode;
 105  111          path = dst_fqi->fq_path.pn_path;
 106  112  
 107  113          /* Check if attempting to rename a stream - not yet supported */
 108  114          rc = smb_rename_check_stream(src_fqi, dst_fqi);
 109  115          if (rc != 0)
 110  116                  return (smb_rename_errno2status(rc));
 111  117  
 112  118          /*
 113  119           * The source node may already have been provided,
 114      -         * i.e. when called by SMB1/SMB2 smb_setinfo_rename.
 115      -         * Not provided by smb_com_rename, smb_com_nt_rename.
      120 +         * i.e. when called by SMB1/SMB2 smb_setinfo_rename
      121 +         * with an ofile.  When we have an ofile, open has
      122 +         * already checked for sharing violations.  For
      123 +         * path-based operations, do sharing check here.
 116  124           */
 117  125          if (src_fqi->fq_fnode) {
 118      -                smb_node_start_crit(src_fqi->fq_fnode, RW_READER);
 119      -                smb_node_ref(src_fqi->fq_fnode);
 120  126                  smb_node_ref(src_fqi->fq_dnode);
      127 +                smb_node_ref(src_fqi->fq_fnode);
      128 +                have_src = B_TRUE;
 121  129          } else {
 122  130                  /* lookup and validate src node */
 123  131                  rc = smb_rename_lookup_src(sr);
 124  132                  if (rc != 0)
 125  133                          return (smb_rename_errno2status(rc));
      134 +                /* Holding refs on dnode, fnode */
 126  135          }
 127      -
 128  136          src_fnode = src_fqi->fq_fnode;
 129  137          src_dnode = src_fqi->fq_dnode;
 130  138  
      139 +        /* Break oplocks, and check share modes. */
      140 +        status = smb_rename_check_src(sr, src_fqi);
      141 +        if (status != NT_STATUS_SUCCESS) {
      142 +                smb_node_release(src_fqi->fq_fnode);
      143 +                smb_node_release(src_fqi->fq_dnode);
      144 +                return (status);
      145 +        }
 131  146          /*
      147 +         * NB: src_fnode is now "in crit" (critical section)
      148 +         * as if we did smb_node_start_crit(..., RW_READER);
      149 +         * Call smb_rename_release_src(sr) on errors.
      150 +         */
      151 +
      152 +        /*
 132  153           * Find the destination dnode and last component.
 133  154           * May already be provided, i.e. when called via
 134  155           * SMB1 trans2 setinfo.
 135  156           */
 136  157          if (dst_fqi->fq_dnode) {
 137  158                  /* called via smb_set_rename_info */
 138  159                  smb_node_ref(dst_fqi->fq_dnode);
 139  160          } else {
 140  161                  /* called via smb2_setf_rename, smb_com_rename, etc. */
 141  162                  rc = smb_pathname_reduce(sr, sr->user_cr, path, tnode, tnode,
↓ open down ↓ 85 lines elided ↑ open up ↑
 227  248                   */
 228  249                  dst_fnode = dst_fqi->fq_fnode;
 229  250  
 230  251                  if (!(sr->arg.dirop.flags && SMB_RENAME_FLAG_OVERWRITE)) {
 231  252                          smb_rename_release_src(sr);
 232  253                          smb_node_release(dst_fnode);
 233  254                          smb_node_release(dst_dnode);
 234  255                          return (NT_STATUS_OBJECT_NAME_COLLISION);
 235  256                  }
 236  257  
 237      -                (void) smb_oplock_break(sr, dst_fnode,
 238      -                    SMB_OPLOCK_BREAK_TO_NONE | SMB_OPLOCK_BREAK_BATCH);
      258 +                status = smb_oplock_break_DELETE(dst_fnode, NULL);
      259 +                if (status == NT_STATUS_OPLOCK_BREAK_IN_PROGRESS) {
      260 +                        if (sr->session->dialect >= SMB_VERS_2_BASE)
      261 +                                (void) smb2sr_go_async(sr);
      262 +                        (void) smb_oplock_wait_break(dst_fnode, 0);
      263 +                        status = 0;
      264 +                }
      265 +                if (status != 0) {
      266 +                        smb_rename_release_src(sr);
      267 +                        smb_node_release(dst_fnode);
      268 +                        smb_node_release(dst_dnode);
      269 +                        return (status);
      270 +                }
 239  271  
 240      -                /*
 241      -                 * Wait (a little) for the oplock break to be
 242      -                 * responded to by clients closing handles.
 243      -                 * Hold node->n_lock as reader to keep new
 244      -                 * ofiles from showing up after we check.
 245      -                 */
 246  272                  smb_node_rdlock(dst_fnode);
 247      -                for (count = 0; count <= 12; count++) {
 248      -                        status = smb_node_delete_check(dst_fnode);
 249      -                        if (status != NT_STATUS_SHARING_VIOLATION)
 250      -                                break;
 251      -                        smb_node_unlock(dst_fnode);
 252      -                        delay(MSEC_TO_TICK(100));
 253      -                        smb_node_rdlock(dst_fnode);
 254      -                }
      273 +                status = smb_node_delete_check(dst_fnode);
 255  274                  if (status != NT_STATUS_SUCCESS) {
 256  275                          smb_node_unlock(dst_fnode);
 257  276                          smb_rename_release_src(sr);
 258  277                          smb_node_release(dst_fnode);
 259  278                          smb_node_release(dst_dnode);
 260  279                          return (NT_STATUS_ACCESS_DENIED);
 261  280                  }
 262  281  
 263  282                  /*
 264  283                   * Note, the combination of these two:
↓ open down ↓ 13 lines elided ↑ open up ↑
 278  297                  status = smb_nbl_conflict(dst_fnode, 0, UINT64_MAX, NBL_REMOVE);
 279  298                  if (status != NT_STATUS_SUCCESS) {
 280  299                          smb_node_end_crit(dst_fnode);
 281  300                          smb_rename_release_src(sr);
 282  301                          smb_node_release(dst_fnode);
 283  302                          smb_node_release(dst_dnode);
 284  303                          return (NT_STATUS_ACCESS_DENIED);
 285  304                  }
 286  305  
 287  306                  new_name = dst_fnode->od_name;
      307 +                dst_exists = B_TRUE;
 288  308          }
 289  309  
      310 +        do_audit = smb_audit_rename_init(sr);
      311 +        /* save paths for later auditing */
      312 +        if (do_audit) {
      313 +                if (!have_src) {
      314 +                        srcpath = kmem_alloc(SMB_MAXPATHLEN, KM_SLEEP);
      315 +                        smb_node_getpath_nofail(src_fnode, smb_audit_rootvp(sr),
      316 +                            srcpath, SMB_MAXPATHLEN);
      317 +                }
      318 +                if (dst_exists) {
      319 +                        dstpath = kmem_alloc(SMB_MAXPATHLEN, KM_SLEEP);
      320 +                        smb_node_getpath_nofail(dst_fnode, smb_audit_rootvp(sr),
      321 +                            dstpath, SMB_MAXPATHLEN);
      322 +                }
      323 +        }
      324 +
 290  325          rc = smb_fsop_rename(sr, sr->user_cr,
 291  326              src_dnode, src_fnode->od_name,
 292  327              dst_dnode, new_name);
 293  328  
      329 +        if (do_audit) {
      330 +                smb_audit_rename_fini(sr,
      331 +                    srcpath,
      332 +                    dst_dnode,
      333 +                    dstpath,
      334 +                    rc == 0,
      335 +                    smb_node_is_dir(src_fnode));
      336 +
      337 +                if (srcpath != NULL)
      338 +                        kmem_free(srcpath, SMB_MAXPATHLEN);
      339 +                if (dstpath != NULL)
      340 +                        kmem_free(dstpath, SMB_MAXPATHLEN);
      341 +        }
      342 +
 294  343          if (rc == 0) {
 295  344                  /*
 296  345                   * Note that renames in the same directory are normally
 297  346                   * delivered in {old,new} pairs, and clients expect them
 298  347                   * in that order, if both events are delivered.
 299  348                   */
 300  349                  int a_src, a_dst; /* action codes */
 301  350                  if (src_dnode == dst_dnode) {
 302  351                          a_src = FILE_ACTION_RENAMED_OLD_NAME;
 303  352                          a_dst = FILE_ACTION_RENAMED_NEW_NAME;
↓ open down ↓ 123 lines elided ↑ open up ↑
 427  476          path = dst_fqi->fq_path.pn_path;
 428  477  
 429  478          /* Cannnot create link on named stream */
 430  479          if (smb_is_stream_name(src_fqi->fq_path.pn_path) ||
 431  480              smb_is_stream_name(dst_fqi->fq_path.pn_path)) {
 432  481                  return (NT_STATUS_INVALID_PARAMETER);
 433  482          }
 434  483  
 435  484          /* The source node may already have been provided */
 436  485          if (src_fqi->fq_fnode) {
 437      -                smb_node_start_crit(src_fqi->fq_fnode, RW_READER);
 438      -                smb_node_ref(src_fqi->fq_fnode);
 439  486                  smb_node_ref(src_fqi->fq_dnode);
      487 +                smb_node_ref(src_fqi->fq_fnode);
 440  488          } else {
 441  489                  /* lookup and validate src node */
 442  490                  rc = smb_rename_lookup_src(sr);
 443  491                  if (rc != 0)
 444  492                          return (smb_rename_errno2status(rc));
      493 +                /* Holding refs on dnode, fnode */
 445  494          }
 446  495  
 447  496          /* Not valid to create hardlink for directory */
 448  497          if (smb_node_is_dir(src_fqi->fq_fnode)) {
 449      -                smb_rename_release_src(sr);
      498 +                smb_node_release(src_fqi->fq_dnode);
      499 +                smb_node_release(src_fqi->fq_fnode);
 450  500                  return (NT_STATUS_FILE_IS_A_DIRECTORY);
 451  501          }
 452  502  
 453  503          /*
      504 +         * Unlike in rename, we will not unlink the src,
      505 +         * so skip the smb_rename_check_src() call, and
      506 +         * just "start crit" instead.
      507 +         */
      508 +        smb_node_start_crit(src_fqi->fq_fnode, RW_READER);
      509 +
      510 +        /*
 454  511           * Find the destination dnode and last component.
 455  512           * May already be provided, i.e. when called via
 456  513           * SMB1 trans2 setinfo.
 457  514           */
 458  515          if (dst_fqi->fq_dnode) {
 459  516                  smb_node_ref(dst_fqi->fq_dnode);
 460  517          } else {
 461  518                  rc = smb_pathname_reduce(sr, sr->user_cr, path, tnode, tnode,
 462  519                      &dst_fqi->fq_dnode, dst_fqi->fq_last_comp);
 463  520                  if (rc != 0) {
↓ open down ↓ 39 lines elided ↑ open up ↑
 503  560          }
 504  561  
 505  562          smb_rename_release_src(sr);
 506  563          smb_node_release(dst_fqi->fq_dnode);
 507  564          return (smb_rename_errno2status(rc));
 508  565  }
 509  566  
 510  567  /*
 511  568   * smb_rename_lookup_src
 512  569   *
 513      - * Lookup the src node, checking for sharing violations and
 514      - * breaking any existing BATCH oplock.
 515      - * Populate sr->arg.dirop.fqi
      570 + * Lookup the src node for a path-based link or rename.
 516  571   *
 517      - * Upon success, the dnode and fnode will have holds and the
 518      - * fnode will be in a critical section. These should be
 519      - * released using smb_rename_release_src().
      572 + * On success, fills in sr->arg.dirop.fqi, and returns with
      573 + * holds on the source dnode and fnode.
 520  574   *
 521  575   * Returns errno values.
 522  576   */
 523  577  static int
 524  578  smb_rename_lookup_src(smb_request_t *sr)
 525  579  {
 526      -        smb_node_t *src_node, *tnode;
 527      -        DWORD status;
 528      -        int rc;
 529      -        int count;
      580 +        smb_node_t *tnode;
 530  581          char *path;
      582 +        int rc;
 531  583  
 532  584          smb_fqi_t *src_fqi = &sr->arg.dirop.fqi;
 533  585  
 534  586          if (smb_is_stream_name(src_fqi->fq_path.pn_path))
 535  587                  return (EINVAL);
 536  588  
 537  589          /* Lookup the source node */
 538  590          tnode = sr->tid_tree->t_snode;
 539  591          path = src_fqi->fq_path.pn_path;
 540  592          rc = smb_pathname_reduce(sr, sr->user_cr, path, tnode, tnode,
 541  593              &src_fqi->fq_dnode, src_fqi->fq_last_comp);
 542  594          if (rc != 0)
 543  595                  return (rc);
      596 +        /* hold fq_dnode */
 544  597  
 545  598          rc = smb_fsop_lookup(sr, sr->user_cr, 0, tnode,
 546  599              src_fqi->fq_dnode, src_fqi->fq_last_comp, &src_fqi->fq_fnode);
 547  600          if (rc != 0) {
 548  601                  smb_node_release(src_fqi->fq_dnode);
 549  602                  return (rc);
 550  603          }
 551      -        src_node = src_fqi->fq_fnode;
      604 +        /* hold fq_dnode, fq_fnode */
 552  605  
 553      -        rc = smb_rename_check_attr(sr, src_node, src_fqi->fq_sattr);
      606 +        rc = smb_rename_check_attr(sr, src_fqi->fq_fnode, src_fqi->fq_sattr);
 554  607          if (rc != 0) {
 555  608                  smb_node_release(src_fqi->fq_fnode);
 556  609                  smb_node_release(src_fqi->fq_dnode);
 557  610                  return (rc);
 558  611          }
 559  612  
      613 +        return (0);
      614 +}
      615 +
      616 +/*
      617 + * smb_rename_check_src
      618 + *
      619 + * Check for sharing violations on the file we'll unlink, and
      620 + * break oplocks for the rename operation.  Note that we've
      621 + * already done oplock breaks associated with opening a handle
      622 + * on the file to rename.
      623 + *
      624 + * On success, returns with fnode in a critical section,
      625 + * as if smb_node_start_crit were called with the node.
      626 + * Caller should release using smb_rename_release_src().
      627 + */
      628 +static uint32_t
      629 +smb_rename_check_src(smb_request_t *sr, smb_fqi_t *src_fqi)
      630 +{
      631 +        smb_node_t *src_node = src_fqi->fq_fnode;
      632 +        uint32_t status;
      633 +
 560  634          /*
 561  635           * Break BATCH oplock before ofile checks. If a client
 562  636           * has a file open, this will force a flush or close,
 563  637           * which may affect the outcome of any share checking.
      638 +         *
      639 +         * This operation may have either a handle or path for
      640 +         * the source node (that will be unlinked via rename).
 564  641           */
 565      -        (void) smb_oplock_break(sr, src_node,
 566      -            SMB_OPLOCK_BREAK_TO_LEVEL_II | SMB_OPLOCK_BREAK_BATCH);
 567  642  
      643 +        if (sr->fid_ofile != NULL) {
      644 +                status = smb_oplock_break_SETINFO(src_node, sr->fid_ofile,
      645 +                    FileRenameInformation);
      646 +                if (status == NT_STATUS_OPLOCK_BREAK_IN_PROGRESS) {
      647 +                        if (sr->session->dialect >= SMB_VERS_2_BASE)
      648 +                                (void) smb2sr_go_async(sr);
      649 +                        (void) smb_oplock_wait_break(src_node, 0);
      650 +                        status = 0;
      651 +                }
      652 +
      653 +                /*
      654 +                 * Sharing violations were checked at open time.
      655 +                 * Just "start crit" to be consistent with the
      656 +                 * state returned for path-based rename.
      657 +                 */
      658 +                smb_node_start_crit(src_fqi->fq_fnode, RW_READER);
      659 +                return (NT_STATUS_SUCCESS);
      660 +        }
      661 +
 568  662          /*
 569      -         * Wait (a little) for the oplock break to be
 570      -         * responded to by clients closing handles.
 571      -         * Hold node->n_lock as reader to keep new
 572      -         * ofiles from showing up after we check.
      663 +         * This code path operates without a real open, so
      664 +         * break oplocks now as if we opened for delete.
      665 +         * Note: SMB2 does only ofile-based rename.
      666 +         *
      667 +         * Todo:  Use an "internal open" for path-based
      668 +         * rename and delete, then delete this code.
 573  669           */
 574      -        smb_node_rdlock(src_node);
 575      -        for (count = 0; count <= 12; count++) {
 576      -                status = smb_node_rename_check(src_node);
 577      -                if (status != NT_STATUS_SHARING_VIOLATION)
 578      -                        break;
 579      -                smb_node_unlock(src_node);
 580      -                delay(MSEC_TO_TICK(100));
 581      -                smb_node_rdlock(src_node);
      670 +        ASSERT(sr->session->dialect < SMB_VERS_2_BASE);
      671 +        status = smb_oplock_break_DELETE(src_node, NULL);
      672 +        if (status == NT_STATUS_OPLOCK_BREAK_IN_PROGRESS) {
      673 +                (void) smb_oplock_wait_break(src_node, 0);
 582  674          }
      675 +
      676 +        /*
      677 +         * Path-based access to the src file (no ofile)
      678 +         * so check for sharing violations here.
      679 +         */
      680 +        smb_node_rdlock(src_node);
      681 +        status = smb_node_rename_check(src_node);
 583  682          if (status != NT_STATUS_SUCCESS) {
 584  683                  smb_node_unlock(src_node);
 585      -                smb_node_release(src_fqi->fq_fnode);
 586      -                smb_node_release(src_fqi->fq_dnode);
 587      -                return (EPIPE); /* = ERRbadshare */
      684 +                return (status);
 588  685          }
 589  686  
      687 +        status = smb_oplock_break_SETINFO(src_node, NULL,
      688 +            FileRenameInformation);
      689 +        if (status == NT_STATUS_OPLOCK_BREAK_IN_PROGRESS) {
      690 +                (void) smb_oplock_wait_break(src_node, 0);
      691 +        }
      692 +
 590  693          /*
 591  694           * Note, the combination of these two:
 592  695           *      smb_node_rdlock(node);
 593  696           *      nbl_start_crit(node->vp, RW_READER);
 594  697           * is equivalent to this call:
 595  698           *      smb_node_start_crit(node, RW_READER)
 596  699           *
 597  700           * Cleanup after this point should use:
 598  701           *      smb_node_end_crit(src_node)
 599  702           */
 600  703          nbl_start_crit(src_node->vp, RW_READER);
 601  704  
 602  705          /*
 603  706           * This checks nbl_share_conflict, nbl_lock_conflict
 604  707           */
 605  708          status = smb_nbl_conflict(src_node, 0, UINT64_MAX, NBL_RENAME);
 606  709          if (status != NT_STATUS_SUCCESS) {
 607  710                  smb_node_end_crit(src_node);
 608      -                smb_node_release(src_fqi->fq_fnode);
 609      -                smb_node_release(src_fqi->fq_dnode);
 610      -                if (status == NT_STATUS_SHARING_VIOLATION)
 611      -                        return (EPIPE); /* = ERRbadshare */
 612      -                return (EACCES);
 613  711          }
 614  712  
 615      -        /* NB: Caller expects holds on src_fqi fnode, dnode */
 616      -        return (0);
      713 +        /* NB: Caller expects to be "in crit" on fnode. */
      714 +        return (status);
 617  715  }
 618  716  
 619  717  /*
 620  718   * smb_rename_release_src
 621  719   */
 622  720  static void
 623  721  smb_rename_release_src(smb_request_t *sr)
 624  722  {
 625  723          smb_fqi_t *src_fqi = &sr->arg.dirop.fqi;
 626  724  
↓ open down ↓ 66 lines elided ↑ open up ↑
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX