Next Previous Contents

Unix Review Column 18 -- Битовые операции

Randal Schwartz

Январь 1998

Перевод Anton Petrusevich <casus@mail.ru> и Alex Ott <ott@phtd.tpu.edu.ru>

Perl имеет много операций вне его назначения, которые делают выполнение многих задач более легким. В этой колонке, мы покажем как операции манипуляции с битами (``bitops'') могут помочь вам в связи с правами доступа к файлам.

Сначала, небольшое введение. Право доступа к файлу (или режим) интерпретируется как 12-битное значение, объединенное в четыре группы по три бита в каждой. Первыми тремя битами являются биты setuid, setgid и sticky, о которых мы не будем здесь говорить. Три другие группы соответственно контролируют права доступа для владельца файла (``пользователь, user''), членов группы-владельца файла (``группа, group'') и всех остальных (``другие, other''). Каждая группа битов контролирует права доступа на запись, чтение и выполнение файла для каждой из групп пользователей.

Мы можем получить число, обозначающее права доступа к конкретному файлу, используя оператор stat, например таким образом:

    $mode = (stat "/etc/motd")[2];
    print $mode;

Но этот код на моей машине печатает непонятное число 33188. Как я могу превратить это число в что-то понятное? Во первых, самым простым способом будет заставить Perl напечатать число в восьмеричном виде, так что группы по три бита появятся как восьмеричные значения 0-7:

    $mode = (stat "/etc/motd")[2];
    printf "%o\n", $mode;

На моей машине этот код выдает значение ``100644''. Чем здесь является дополнительная цифра 1? Этот код показывает, что /etc/motd является файлом, ен похожем на устройство или каталог. Мы не хотим видеть его, так что здесь должны использоваться битовые операторы. Давайте выполним побитовую операцию "И" нашего значения с числом 07777, показывая только права доступа, а не закодированный тип:

    $mode = (stat "/etc/motd")[2];
    printf "%o\n", $mode & 07777;

Это является хорошо знакомым значением ``644'', которое мы передаем команде chmod. Теперь как я могу сделать, чтобы файл был доступен для записи для всех? (Не рекомендуется... просто для примера...) Нам необходимо выполнить операцию or (или) над битами, которые представляют право записи для пользователя, группы и остальных, и которые записываются как 0222 (другое восьмеричное значение):

    $mode = (stat "/etc/motd")[2];
    $newmode = $mode | 0222;
    chmod $newmode, "/etc/motd";

Здесь использование ``|'' заставляет выполнить операцию ``или'' с заданными битами, и мы получаем доступный для записи всеми пользователями файл сообщений (если вы конечно администратор).

Как мы можем просто выделить группу битов? Для этого нам необходимо выполнить операцию сдвига, вместе с выполнением операции ``и'', например так:

    $mode = (stat "/etc/motd")[2];
    $groupmode = ($mode & 070) >> 3;
    print "$groupmode\n";

Этот код на моей машине выдает ``4''. Заметьте, что я не использую функцию printf, поскольку восьмеричные 0-7 выдаются также как и десятичные числа, в том же самом диапазоне.

Так, что вместо продолжения показа частичных примеров, давайте применим это новое значение в конкретном куске кода. Эта программа бедет проходить по вашим путям поиска, в поиске каталогов, которые могут быть изменены пользователями, к которым нет доверия. Этот код начинается с чего-то подобного этому коду:

   #!/usr/bin/perl
   use File::stat;

Модуль File::stat позволяет нам получить именованный доступ к некоторым полям stat. Смотрите применение этого в дальнейшем коде. Далее у нас имеется код:

    sub trust_user { $_[0] < 100 }
    sub trust_group { $_[0] < 100 }

Эти подпрограммы определяют номера пользователей и групп, которым имеют нужные права. Предполагается, что каталог доступный для записи таким пользователям, является безопасным. Далее, разделим пути поиска на понятные компоненты с помощью кода:

    my @path = split /:/, $ENV{PATH};

И затем нам необходимо пройти по путям поиска, по каждому из элементов:

    for (@path) {
      (my $statinfo = stat $_) or
        (warn "cannot stat $_: $!"), next;

Заметьте, что мы сначала помещаем каждое из имен в переменную $_, а затем пытаемся определить ее параметры. Вышеприведенный оператор use переопределяет функцию stat, так что она будет возвращать объект, который отвечает на вызов функций, названных как имена полей функции stat. Так, что возвращаемое значение является скаляром, а не списком.

Далее мы устанавливаем некоторые переменные:

      my @reasons;
      my $uid = $statinfo->uid;
      my $gid = $statinfo->gid;
      my $mode = $statinfo->mode;

Переменная @reasons содержит причины, по которым каталог считается плохим. В начале, это список пуст. Другие три переменные содержат номер владельца, группы-владельца и значение прав доступа и типа, как это описано выше. Далее давайте проверим права доступа владельца:

      if ($uid != $<
          and not trust_user($uid)
          and $mode & 0200) {
        push @reasons, "non-owned, write by owner ".
          getpwuid($uid);
      }

Здесь, если владелец каталога не является человеком, запустившем скрипт, и если владелец каталога не входит в список доверенных пользователей, то содержимое каталога может быть изменено кем-то, если он также имеет права записи в этот каталог. Так, что мы проверим это путем выполнения операции ``и'' между $mode и числом 0200, результат которой будет ненулевым, если установлен бит доступа на запись для пользователя.

Если это так, то мы добавляем человека, к которому нет доверия к списку причин, используя операцию push для текстового сообщения. Вызов getpwuid возвращает нам имя пользователя вместо его номера.

Далее, выполняется тоже самое (приблизительно) с учетом бита права доступа на запись для пользователя:

      if (not trust_group($gid)
          and $mode & 020) {
        push @reasons, "write by group ".
          getgrgid($gid);
      }

И затем для бита записи для остальных пользователей:

      if ($mode & 02) {
        push @reasons, "write by world";
      }

В заключение, после всех причин, которые могут быть неправильными, настало время показать найденные ошибки:

      if (@reasons) {
        print
          "danger: $_ is dangerous because ",
          "of the following reasons:\n",
          map "  $_\n", @reasons;
      }
    }

В этом эпизоде, если @reasons является не пустым, то пришло время протеста. Оператор map превращает каждую из причин в сообщение с отступом, которые затем предваряются общим сообщением об ошибке. А также приведена дополнительная закрывающая скобка для соответствия оператору цикла foreach, который начат ранее.

Так, что если вы поместите этот код в файл и запустите его, то вы увидите все места, в которых вам могут нанести ущерб. Убедите, что вы правильно настроили список доверенных пользователей и групп для соответствия вашей настройке.

А сейчас, давайте рассмотрим еще одну небольшую программу. Я попытался сделать из нее эквивалент команде:

     chmod -R go=u-w /some/dir /other/dir

которую я выполняю очень часто, поскольку она устанавливает все права доступа в правильные значения для ``опубликованного'' дерева каталогов, независимо от того, являются ли они исполнимыми файлами, или чем-то другим.

Но меня раздражало то, что команда chmod не имеет ключа для ``многословного'' режима, который позволил бы не узнать о том, что идет неправильно, и что должно быть исправлено. Я также хотел отметить некоторые каталоги как ``не затрагиваемые'', что является ключом, которого также не имеет стандартная команда chmod.

Но никогда не бойтесь... прием (hack) на языке Perl спасет вас! Эта маленькая программа начинается со строк:

    #!/usr/bin/perl
    $| = 1;
    use File::Find;

Эти строки отключают буферизацию STDOUT и подключают модуль File::Find. Далее мы используем код верхнего уровня. Просто одна строка:

    find \&wanted, @ARGV;

Эта строка вызывает функцию find, определенную в модуле File::Find, передавая ей подпрограмму, определенную ниже и аргументы командной строки. Я описывал File::Find в предыдущих выпусках. Подпрограмма &wanted будет вызываться для каждого из путевых имен, и начинается со следующего кода:

     sub wanted {
      if (-d && -e "$_/.PRIVATE") {
        print "## skipping contents of $File::Find::name\n";
        return $File::Find::prune = 1;
      }

Сначала мы определяем каждый каталог, который содержит каталог с именем .PRIVATE. Так я называю собственные ``не затрагиваемые'' области. Установка переменной $File::Find::prune в значение 1 заставляет File::Find::find прекратить нисходящий переход в эту область. Далее мы пропускаем, символические ссылки и все не являющееся файлами или каталогами:

      return if -l;
      return unless -f or -d;

И также определяем то, для чего не можем выполнить функцию stat:

    my @stat = stat or return;

Далее мы получаем различные биты режима:

      my $mode = $stat[2] & 07777;
      my $user_rwx = ($mode & 00700) >> 6;
      my $target_bits = $user_rwx & 05;
      my $group_rwx = ($mode & 00070) >> 3;
      my $other_rwx = ($mode & 00007);

Здесь... $user_rwx является правами пользователя, в то время как $target_bits являются тем, чем должны быть права доступа для группы и остальных пользователей. Давайте посмотрим, если они являются:

 
     if ($target_bits != $group_rwx
          or $target_bits != $other_rwx) {

Далее, если эта проверка не проходит, то нам необходимо вычислить новый режим:

 
     my $new_mode = (($mode & 07700) |
                        ($target_bits << 3) |
                        ($target_bits));

Заметьте использование здесь побитовой операции ``или'' для создания нового режима из частей. В заключение, давайте сообщим о том, что мы делаем и сделаем это:

        printf
          "chmod %o %s # was %o\n",
          $new_mode, $File::Find::name, $mode;
        chmod $new_mode, $_;
      }
    }

И заключительный скобки закрывают определение подпрограммы.

Таким образом вы можете подружиться с битовыми операциями. Узнайте и используйте их. И возможно в следующий раз, когда вы будете проклинать стандартные утилиты Unix за то, что они не имеют ключа ``многословного'' режима, то вы сможете заменить эти утилиты 10-строковой программой на Perl, которая выполняет ту же самую работу, с практически той же скоростью. Наслаждайтесь!


Next Previous Contents