Print this page
    
usr/src/cmd/dlmgmtd/dlmgmt_door.c
    
      
        | Split | 
	Close | 
      
      | Expand all | 
      | Collapse all | 
    
    
          --- old/usr/src/cmd/dlmgmtd/dlmgmt_main.c
          +++ new/usr/src/cmd/dlmgmtd/dlmgmt_main.c
   1    1  /*
   2    2   * CDDL HEADER START
   3    3   *
   4    4   * The contents of this file are subject to the terms of the
   5    5   * Common Development and Distribution License (the "License").
   6    6   * You may not use this file except in compliance with the License.
   7    7   *
   8    8   * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
   9    9   * or http://www.opensolaris.org/os/licensing.
  10   10   * See the License for the specific language governing permissions
  11   11   * and limitations under the License.
  12   12   *
  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  /*
  23   23   * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
  24   24   * Use is subject to license terms.
  25   25   * Copyright 2016 Joyent, Inc.
  26   26   */
  27   27  
  28   28  /*
  29   29   * The dlmgmtd daemon is started by the datalink-management SMF service.
  30   30   * This daemon is used to manage <link name, linkid> mapping and the
  31   31   * persistent datalink configuration.
  32   32   *
  33   33   * Today, the <link name, linkid> mapping and the persistent configuration
  34   34   * of datalinks is kept in /etc/dladm/datalink.conf, and the daemon keeps
  35   35   * a copy of the datalinks in the memory (see dlmgmt_id_avl and
  36   36   * dlmgmt_name_avl). The active <link name, linkid> mapping is kept in
  37   37   * /etc/svc/volatile/dladm cache file, so that the mapping can be recovered
  38   38   * when dlmgmtd exits for some reason (e.g., when dlmgmtd is accidentally
  39   39   * killed).
  40   40   */
  41   41  
  42   42  #include <assert.h>
  43   43  #include <errno.h>
  44   44  #include <fcntl.h>
  45   45  #include <priv.h>
  46   46  #include <signal.h>
  47   47  #include <stdlib.h>
  48   48  #include <stdio.h>
  49   49  #include <strings.h>
  50   50  #include <syslog.h>
  51   51  #include <zone.h>
  52   52  #include <sys/dld.h>
  53   53  #include <sys/dld_ioc.h>
  54   54  #include <sys/param.h>
  55   55  #include <sys/stat.h>
  56   56  #include <unistd.h>
  57   57  #include <libdladm_impl.h>
  58   58  #include <libdlmgmt.h>
  59   59  #include "dlmgmt_impl.h"
  60   60  
  61   61  const char              *progname;
  62   62  boolean_t               debug;
  63   63  static int              pfds[2];
  64   64  /*
  65   65   * This file descriptor to DLMGMT_DOOR cannot be in the libdladm
  66   66   * handle because the door isn't created when the handle is created.
  67   67   */
  68   68  static int              dlmgmt_door_fd = -1;
  69   69  
  70   70  /*
  71   71   * This libdladm handle is global so that dlmgmt_upcall_linkprop_init() can
  72   72   * pass to libdladm.  The handle is opened with "ALL" privileges, before
  73   73   * privileges are dropped in dlmgmt_drop_privileges().  It is not able to open
  74   74   * DLMGMT_DOOR at that time as it hasn't been created yet.  This door in the
  75   75   * handle is opened in the first call to dladm_door_fd().
  76   76   */
  77   77  dladm_handle_t          dld_handle = NULL;
  78   78  
  79   79  static void             dlmgmtd_exit(int);
  80   80  static int              dlmgmt_init();
  81   81  static void             dlmgmt_fini();
  82   82  static int              dlmgmt_set_privileges();
  83   83  
  84   84  static int
  85   85  dlmgmt_set_doorfd(boolean_t start)
  86   86  {
  87   87          dld_ioc_door_t did;
  88   88          int err = 0;
  89   89  
  90   90          assert(dld_handle != NULL);
  91   91  
  92   92          did.did_start_door = start;
  93   93  
  94   94          if (ioctl(dladm_dld_fd(dld_handle), DLDIOC_DOORSERVER, &did) == -1)
  95   95                  err = errno;
  96   96  
  97   97          return (err);
  98   98  }
  99   99  
 100  100  static int
 101  101  dlmgmt_door_init(void)
 102  102  {
 103  103          int err = 0;
 104  104  
 105  105          if ((dlmgmt_door_fd = door_create(dlmgmt_handler, NULL,
 106  106              DOOR_REFUSE_DESC | DOOR_NO_CANCEL)) == -1) {
 107  107                  err = errno;
 108  108                  dlmgmt_log(LOG_ERR, "door_create() failed: %s",
 109  109                      strerror(err));
 110  110                  return (err);
 111  111          }
 112  112          return (err);
 113  113  }
 114  114  
 115  115  static void
 116  116  dlmgmt_door_fini(void)
 117  117  {
 118  118          if (dlmgmt_door_fd == -1)
 119  119                  return;
 120  120  
 121  121          if (door_revoke(dlmgmt_door_fd) == -1) {
 122  122                  dlmgmt_log(LOG_WARNING, "door_revoke(%s) failed: %s",
 123  123                      DLMGMT_DOOR, strerror(errno));
 124  124          }
 125  125          (void) dlmgmt_set_doorfd(B_FALSE);
 126  126          dlmgmt_door_fd = -1;
 127  127  }
 128  128  
 129  129  static int
 130  130  dlmgmt_door_attach(zoneid_t zoneid, char *rootdir)
 131  131  {
 132  132          int     fd;
 133  133          int     err = 0;
 134  134          char    doorpath[MAXPATHLEN];
 135  135          struct stat statbuf;
 136  136  
 137  137          /* Handle running in a non-native branded zone (i.e. has /native) */
 138  138          (void) snprintf(doorpath, sizeof (doorpath), "%s/native%s",
 139  139              rootdir, DLMGMT_TMPFS_DIR);
 140  140          if (stat(doorpath, &statbuf) == 0) {
 141  141                  (void) snprintf(doorpath, sizeof (doorpath), "%s/native%s",
 142  142                      rootdir, DLMGMT_DOOR);
 143  143          } else {
 144  144                  (void) snprintf(doorpath, sizeof (doorpath), "%s%s",
 145  145                      rootdir, DLMGMT_DOOR);
 146  146          }
 147  147  
 148  148          /*
 149  149           * Create the door file for dlmgmtd.
 150  150           */
 151  151          if ((fd = open(doorpath, O_CREAT|O_RDONLY, 0644)) == -1) {
 152  152                  err = errno;
 153  153                  dlmgmt_log(LOG_ERR, "open(%s) failed: %s", doorpath,
 154  154                      strerror(err));
 155  155                  return (err);
 156  156          }
 157  157          (void) close(fd);
 158  158          if (chown(doorpath, UID_DLADM, GID_NETADM) == -1)
 159  159                  return (errno);
 160  160  
 161  161          /*
 162  162           * fdetach first in case a previous daemon instance exited
 163  163           * ungracefully.
 164  164           */
 165  165          (void) fdetach(doorpath);
 166  166          if (fattach(dlmgmt_door_fd, doorpath) != 0) {
 167  167                  err = errno;
 168  168                  dlmgmt_log(LOG_ERR, "fattach(%s) failed: %s", doorpath,
 169  169                      strerror(err));
 170  170          } else if (zoneid == GLOBAL_ZONEID) {
 171  171                  if ((err = dlmgmt_set_doorfd(B_TRUE)) != 0) {
 172  172                          dlmgmt_log(LOG_ERR, "cannot set kernel doorfd: %s",
 173  173                              strerror(err));
 174  174                  }
 175  175          }
 176  176  
 177  177          return (err);
 178  178  }
 179  179  
 180  180  /*
 181  181   * Create the /etc/svc/volatile/dladm/ directory if it doesn't exist, load the
 182  182   * datalink.conf data for this zone, and create/attach the door rendezvous
 183  183   * file.
 184  184   */
 185  185  int
 186  186  dlmgmt_zone_init(zoneid_t zoneid)
 187  187  {
 188  188          char    rootdir[MAXPATHLEN], tmpfsdir[MAXPATHLEN];
 189  189          int     err;
 190  190          struct stat statbuf;
 191  191  
 192  192          if (zoneid == GLOBAL_ZONEID) {
 193  193                  rootdir[0] = '\0';
 194  194          } else if (zone_getattr(zoneid, ZONE_ATTR_ROOT, rootdir,
 195  195              sizeof (rootdir)) < 0) {
 196  196                  return (errno);
 197  197          }
 198  198  
 199  199          /*
 200  200           * Create the DLMGMT_TMPFS_DIR directory.
 201  201           */
 202  202          (void) snprintf(tmpfsdir, sizeof (tmpfsdir), "%s%s", rootdir,
 203  203              DLMGMT_TMPFS_DIR);
 204  204          if (stat(tmpfsdir, &statbuf) < 0) {
 205  205                  if (mkdir(tmpfsdir, (mode_t)0755) < 0) {
 206  206                          /*
 207  207                           * Handle running in a non-native branded zone
 208  208                           * (i.e. has /native)
 209  209                           */
 210  210                          (void) snprintf(tmpfsdir, sizeof (tmpfsdir),
 211  211                              "%s/native%s", rootdir, DLMGMT_TMPFS_DIR);
 212  212                          if (mkdir(tmpfsdir, (mode_t)0755) < 0)
 213  213                                  return (errno);
 214  214                  }
 215  215          } else if ((statbuf.st_mode & S_IFMT) != S_IFDIR) {
 216  216                  return (ENOTDIR);
 217  217          }
 218  218  
 219  219          if ((chmod(tmpfsdir, 0755) < 0) ||
 220  220              (chown(tmpfsdir, UID_DLADM, GID_NETADM) < 0)) {
 221  221                  return (EPERM);
 222  222          }
 223  223  
 224  224          if ((err = dlmgmt_db_init(zoneid, rootdir)) != 0)
 225  225                  return (err);
 226  226          return (dlmgmt_door_attach(zoneid, rootdir));
 227  227  }
 228  228  
 229  229  /*
 230  230   * Initialize each running zone.
 231  231   */
 232  232  static int
 233  233  dlmgmt_allzones_init(void)
 234  234  {
 235  235          int             i;
 236  236          zoneid_t        *zids = NULL;
 237  237          uint_t          nzids, nzids_saved;
 238  238  
 239  239          if (zone_list(NULL, &nzids) != 0)
 240  240                  return (errno);
 241  241  again:
 242  242          nzids *= 2;
 243  243          if ((zids = malloc(nzids * sizeof (zoneid_t))) == NULL)
 244  244                  return (errno);
 245  245          nzids_saved = nzids;
 246  246          if (zone_list(zids, &nzids) != 0) {
 247  247                  free(zids);
 248  248                  return (errno);
 249  249          }
 250  250          if (nzids > nzids_saved) {
 251  251                  free(zids);
 252  252                  goto again;
 253  253          }
 254  254  
  
    | 
      ↓ open down ↓ | 
    254 lines elided | 
    
      ↑ open up ↑ | 
  
 255  255          for (i = 0; i < nzids; i++) {
 256  256                  int res;
 257  257                  zone_status_t status;
 258  258  
 259  259                  /*
 260  260                   * Skip over zones that have gone away or are going down
 261  261                   * since we got the list.  Process all zones in the list,
 262  262                   * logging errors for any that failed.
 263  263                   */
 264  264                  if (zone_getattr(zids[i], ZONE_ATTR_STATUS, &status,
 265      -                    sizeof (status)) < 0)
      265 +                    sizeof (status)) < 0) {
 266  266                          continue;
      267 +                }
 267  268                  switch (status) {
 268  269                          case ZONE_IS_SHUTTING_DOWN:
 269  270                          case ZONE_IS_EMPTY:
 270  271                          case ZONE_IS_DOWN:
 271  272                          case ZONE_IS_DYING:
 272  273                          case ZONE_IS_DEAD:
 273      -                                /* FALLTHRU */
      274 +                        case ZONE_IS_INITIALIZED:
      275 +                        case ZONE_IS_UNINITIALIZED:
 274  276                                  continue;
 275  277                          default:
 276  278                                  break;
 277  279                  }
 278  280                  if ((res = dlmgmt_zone_init(zids[i])) != 0) {
 279  281                          (void) fprintf(stderr, "zone (%ld) init error %s",
 280  282                              zids[i], strerror(res));
 281  283                          dlmgmt_log(LOG_ERR, "zone (%d) init error %s",
 282  284                              zids[i], strerror(res));
 283  285                  }
 284  286          }
 285  287          free(zids);
 286  288          return (0);
 287  289  }
 288  290  
 289  291  static int
 290  292  dlmgmt_init(void)
 291  293  {
 292  294          int     err;
 293  295          char    *fmri, *c;
 294  296          char    filename[MAXPATHLEN];
 295  297  
 296  298          if (dladm_open(&dld_handle) != DLADM_STATUS_OK) {
 297  299                  dlmgmt_log(LOG_ERR, "dladm_open() failed");
 298  300                  return (EPERM);
 299  301          }
 300  302  
 301  303          if (signal(SIGTERM, dlmgmtd_exit) == SIG_ERR ||
 302  304              signal(SIGINT, dlmgmtd_exit) == SIG_ERR) {
 303  305                  err = errno;
 304  306                  dlmgmt_log(LOG_ERR, "signal() for SIGTERM/INT failed: %s",
 305  307                      strerror(err));
 306  308                  return (err);
 307  309          }
 308  310  
 309  311          /*
 310  312           * First derive the name of the cache file from the FMRI name. This
 311  313           * cache name is used to keep active datalink configuration.
 312  314           */
 313  315          if (debug) {
 314  316                  (void) snprintf(cachefile, MAXPATHLEN, "%s/%s%s",
 315  317                      DLMGMT_TMPFS_DIR, progname, ".debug.cache");
 316  318          } else {
 317  319                  if ((fmri = getenv("SMF_FMRI")) == NULL) {
 318  320                          dlmgmt_log(LOG_ERR, "dlmgmtd is an smf(7) managed "
 319  321                              "service and should not be run from the command "
 320  322                              "line.");
 321  323                          return (EINVAL);
 322  324                  }
 323  325  
 324  326                  /*
 325  327                   * The FMRI name is in the form of
 326  328                   * svc:/service/service:instance.  We need to remove the
 327  329                   * prefix "svc:/" and replace '/' with '-'.  The cache file
 328  330                   * name is in the form of "service:instance.cache".
 329  331                   */
 330  332                  if ((c = strchr(fmri, '/')) != NULL)
 331  333                          c++;
 332  334                  else
 333  335                          c = fmri;
 334  336                  (void) snprintf(filename, MAXPATHLEN, "%s.cache", c);
 335  337                  c = filename;
 336  338                  while ((c = strchr(c, '/')) != NULL)
 337  339                          *c = '-';
 338  340  
 339  341                  (void) snprintf(cachefile, MAXPATHLEN, "%s/%s",
 340  342                      DLMGMT_TMPFS_DIR, filename);
 341  343          }
 342  344  
 343  345          dlmgmt_linktable_init();
 344  346          if ((err = dlmgmt_door_init()) != 0)
 345  347                  goto done;
 346  348  
 347  349          /*
 348  350           * Load datalink configuration and create dlmgmtd door files for all
 349  351           * currently running zones.
 350  352           */
 351  353          if ((err = dlmgmt_allzones_init()) != 0)
 352  354                  dlmgmt_door_fini();
 353  355  
 354  356  done:
 355  357          if (err != 0)
 356  358                  dlmgmt_linktable_fini();
 357  359          return (err);
 358  360  }
 359  361  
 360  362  static void
 361  363  dlmgmt_fini(void)
 362  364  {
 363  365          dlmgmt_door_fini();
 364  366          dlmgmt_linktable_fini();
 365  367          if (dld_handle != NULL) {
 366  368                  dladm_close(dld_handle);
 367  369                  dld_handle = NULL;
 368  370          }
 369  371  }
 370  372  
 371  373  /*
 372  374   * This is called by the child process to inform the parent process to
 373  375   * exit with the given return value.
 374  376   */
 375  377  static void
 376  378  dlmgmt_inform_parent_exit(int rv)
 377  379  {
 378  380          if (debug)
 379  381                  return;
 380  382  
 381  383          if (write(pfds[1], &rv, sizeof (int)) != sizeof (int)) {
 382  384                  dlmgmt_log(LOG_WARNING,
 383  385                      "dlmgmt_inform_parent_exit() failed: %s", strerror(errno));
 384  386                  (void) close(pfds[1]);
 385  387                  exit(EXIT_FAILURE);
 386  388          }
 387  389          (void) close(pfds[1]);
 388  390  }
 389  391  
 390  392  /*ARGSUSED*/
 391  393  static void
 392  394  dlmgmtd_exit(int signo)
 393  395  {
 394  396          (void) close(pfds[1]);
 395  397          dlmgmt_fini();
 396  398          exit(EXIT_FAILURE);
 397  399  }
 398  400  
 399  401  static void
 400  402  usage(void)
 401  403  {
 402  404          (void) fprintf(stderr, "Usage: %s [-d]\n", progname);
 403  405          exit(EXIT_FAILURE);
 404  406  }
 405  407  
 406  408  /*
 407  409   * Restrict privileges to only those needed.
 408  410   */
 409  411  int
 410  412  dlmgmt_drop_privileges(void)
 411  413  {
 412  414          priv_set_t      *pset;
 413  415          priv_ptype_t    ptype;
 414  416          zoneid_t        zoneid = getzoneid();
 415  417          int             err = 0;
 416  418  
 417  419          if ((pset = priv_allocset()) == NULL)
 418  420                  return (errno);
 419  421  
 420  422          /*
 421  423           * The global zone needs PRIV_PROC_FORK so that it can fork() when it
 422  424           * issues db ops in non-global zones, PRIV_SYS_CONFIG to post
 423  425           * sysevents, and PRIV_SYS_DL_CONFIG to initialize link properties in
 424  426           * dlmgmt_upcall_linkprop_init().
 425  427           *
 426  428           * We remove non-basic privileges from the permitted (and thus
 427  429           * effective) set.  When executing in a non-global zone, dlmgmtd
 428  430           * only needs to read and write to files that it already owns.
 429  431           */
 430  432          priv_basicset(pset);
 431  433          (void) priv_delset(pset, PRIV_PROC_EXEC);
 432  434          (void) priv_delset(pset, PRIV_PROC_INFO);
 433  435          (void) priv_delset(pset, PRIV_PROC_SESSION);
 434  436          (void) priv_delset(pset, PRIV_FILE_LINK_ANY);
 435  437          if (zoneid == GLOBAL_ZONEID) {
 436  438                  ptype = PRIV_EFFECTIVE;
 437  439                  if (priv_addset(pset, PRIV_SYS_CONFIG) == -1 ||
 438  440                      priv_addset(pset, PRIV_SYS_DL_CONFIG) == -1)
 439  441                          err = errno;
 440  442          } else {
 441  443                  (void) priv_delset(pset, PRIV_PROC_FORK);
 442  444                  ptype = PRIV_PERMITTED;
 443  445          }
 444  446          if (err == 0 && setppriv(PRIV_SET, ptype, pset) == -1)
 445  447                  err = errno;
 446  448  done:
 447  449          priv_freeset(pset);
 448  450          return (err);
 449  451  }
 450  452  
 451  453  int
 452  454  dlmgmt_elevate_privileges(void)
 453  455  {
 454  456          priv_set_t      *privset;
 455  457          int             err = 0;
 456  458  
 457  459          if ((privset = priv_str_to_set("zone", ",", NULL)) == NULL)
 458  460                  return (errno);
 459  461          if (setppriv(PRIV_SET, PRIV_EFFECTIVE, privset) == -1)
 460  462                  err = errno;
 461  463          priv_freeset(privset);
 462  464          return (err);
 463  465  }
 464  466  
 465  467  /*
 466  468   * Set the uid of this daemon to the "dladm" user and drop privileges to only
 467  469   * those needed.
 468  470   */
 469  471  static int
 470  472  dlmgmt_set_privileges(void)
 471  473  {
 472  474          int err;
 473  475  
 474  476          (void) setgroups(0, NULL);
 475  477          if (setegid(GID_NETADM) == -1 || seteuid(UID_DLADM) == -1)
 476  478                  err = errno;
 477  479          else
 478  480                  err = dlmgmt_drop_privileges();
 479  481  done:
 480  482          return (err);
 481  483  }
 482  484  
 483  485  /*
 484  486   * Keep the pfds fd open, close other fds.
 485  487   */
 486  488  /*ARGSUSED*/
 487  489  static int
 488  490  closefunc(void *arg, int fd)
 489  491  {
 490  492          if (fd != pfds[1])
 491  493                  (void) close(fd);
 492  494          return (0);
 493  495  }
 494  496  
 495  497  static boolean_t
 496  498  dlmgmt_daemonize(void)
 497  499  {
 498  500          pid_t pid;
 499  501          int rv;
 500  502  
 501  503          if (pipe(pfds) < 0) {
 502  504                  (void) fprintf(stderr, "%s: pipe() failed: %s\n",
 503  505                      progname, strerror(errno));
 504  506                  exit(EXIT_FAILURE);
 505  507          }
 506  508  
 507  509          if ((pid = fork()) == -1) {
 508  510                  (void) fprintf(stderr, "%s: fork() failed: %s\n",
 509  511                      progname, strerror(errno));
 510  512                  exit(EXIT_FAILURE);
 511  513          } else if (pid > 0) { /* Parent */
 512  514                  (void) close(pfds[1]);
 513  515  
 514  516                  /*
 515  517                   * Read the child process's return value from the pfds.
 516  518                   * If the child process exits unexpected, read() returns -1.
 517  519                   */
 518  520                  if (read(pfds[0], &rv, sizeof (int)) != sizeof (int)) {
 519  521                          (void) kill(pid, SIGKILL);
 520  522                          rv = EXIT_FAILURE;
 521  523                  }
 522  524  
 523  525                  (void) close(pfds[0]);
 524  526                  exit(rv);
 525  527          }
 526  528  
 527  529          /* Child */
 528  530          (void) close(pfds[0]);
 529  531          (void) setsid();
 530  532  
 531  533          /*
 532  534           * Close all files except pfds[1].
 533  535           */
 534  536          (void) fdwalk(closefunc, NULL);
 535  537          (void) chdir("/");
 536  538          openlog(progname, LOG_PID, LOG_DAEMON);
 537  539          return (B_TRUE);
 538  540  }
 539  541  
 540  542  int
 541  543  main(int argc, char *argv[])
 542  544  {
 543  545          int opt, err;
 544  546  
 545  547          progname = strrchr(argv[0], '/');
 546  548          if (progname != NULL)
 547  549                  progname++;
 548  550          else
 549  551                  progname = argv[0];
 550  552  
 551  553          /*
 552  554           * Process options.
 553  555           */
 554  556          while ((opt = getopt(argc, argv, "d")) != EOF) {
 555  557                  switch (opt) {
 556  558                  case 'd':
 557  559                          debug = B_TRUE;
 558  560                          break;
 559  561                  default:
 560  562                          usage();
 561  563                  }
 562  564          }
 563  565  
 564  566          if (!debug && !dlmgmt_daemonize())
 565  567                  return (EXIT_FAILURE);
 566  568  
 567  569          if ((err = dlmgmt_init()) != 0) {
 568  570                  dlmgmt_log(LOG_ERR, "unable to initialize daemon: %s",
 569  571                      strerror(err));
 570  572                  goto child_out;
 571  573          } else if ((err = dlmgmt_set_privileges()) != 0) {
 572  574                  dlmgmt_log(LOG_ERR, "unable to set daemon privileges: %s",
 573  575                      strerror(err));
 574  576                  dlmgmt_fini();
 575  577                  goto child_out;
 576  578          }
 577  579  
 578  580          /*
 579  581           * Inform the parent process that it can successfully exit.
 580  582           */
 581  583          dlmgmt_inform_parent_exit(EXIT_SUCCESS);
 582  584  
 583  585          for (;;)
 584  586                  (void) pause();
 585  587  
 586  588  child_out:
 587  589          /* return from main() forcibly exits an MT process */
 588  590          dlmgmt_inform_parent_exit(EXIT_FAILURE);
 589  591          return (EXIT_FAILURE);
 590  592  }
  
    | 
      ↓ open down ↓ | 
    307 lines elided | 
    
      ↑ open up ↑ | 
  
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX