Блог пользователя Stigius

Автор Stigius, 12 лет назад, По-русски

Как многие знают, вызов BigInteger.isProbablePrime в решениях на java на тимусе приводил к вердикту "Restricted Function". Сегодня я решил разобраться с этой проблемой.

Когда я начал читать исходник BigInteger.java, я обнаружил, что метод "isProbablePrime" вызывает "primeToCertainty" со значением null в качестве параметра "random". Этот null передаётся дальше в метод "passesMillerRabin", который содержит следующий код:

if (rnd == null) {
    rnd = getSecureRandom();
}

То есть, если "rnd" был null, а в нашем случае это именно так, то вызывается "getSecureRandom". У него логика следующая:

private static volatile Random staticRandom;
private static Random getSecureRandom() {
	if (staticRandom == null) {
		staticRandom = new java.security.SecureRandom();
	}
	return staticRandom;
}

То есть когда он вызывается в первый раз, он инициализирует статический объект "staticRandom" с помощью "java.security.SecureRandom", а дальше уже использует этот объект для последующих использований.

Вызов "java.security.SecureRandom" и является корнем всех зол. Поскольку он является "безопасным", то для инициализации он собирает кучу разной информации, включая текущее время и ip-адрес машины, на которой вызван. Это создаёт две больших проблемы. Во-первых, поведение становится недетерминированным. Т.е. при двух запусках программы из-за разной инициализации рандома isProbablePrime может возвращать разные результаты, и, как следствие, меняется весь вывод программы. Это создаёт определённые проблемы при реджаджах. Во-вторых, все вызовы к сетевой подсистеме заблокированы из соображений безопасности, что, собственно говоря, и приводило к получаемому вердикту. Более того, когда мы попробовали разрешить эти вызовы, по непонятным причинам такая инициализация рандома занимала чуть более 5 секунд. Поэтому мы решили пойти по немного другому пути. А именно — вызов SecureRandom мы заменили на обычный Random с некоторым константным сидом и подменили BigInteger.class новым, пропатченным. Это успешно решило обе проблемы и теперь BigInteger.isProbablePrime работает как положено. Все пострадавшие решения уже перепроверены, 44 решения получили Accepted.

  • Проголосовать: нравится
  • +50
  • Проголосовать: не нравится

»
12 лет назад, # |
  Проголосовать: нравится +11 Проголосовать: не нравится

Кстати, вопрос знающим людям. Как подобные проблемы решаются в других проверяющих системах, в частности, на NEERC'е?

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится +19 Проголосовать: не нравится

    Вроде бы можно выставить свойство java.security.egd чтобы указать, откуда брать энтропию

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится +20 Проголосовать: не нравится

    Не знаю, как эта проблема решается в PCMS2 и Ejudge, но пытаться запрещать системные вызовы — какая-то злая идея. Гораздо разумнее подсунуть программе фейковое окружение, и пусть получает текущее время или ip-адрес сколько душе угодно.

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится +3 Проголосовать: не нравится

      Для управления разрешениями в Java есть встроенный механизм. Системные вызовы в данном случае оказались запрещены случайно. Но, как написал Stigius, снятие этих ограничений не решило проблему — Restricted function сменился на Time limit exceeded.

      На правах флейма. Код в Java выполняется внутри виртуальной машины JVM. Ее можно поместить в песочницу, то есть, например, еще в одну виртуальную машину. И все это запускать на виртуальном сервере. Таким образом получаем три уровня виртуализации :)

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        Ну т.е. для каждого языка у вас какие-то свои средства обеспечения безопасности? Выглядит костыльно. Давно уже существуют механизмы виртуализации, достаточно быстрые для использования при тестировании (см. например LXC; инициализировать новый контейнер там можно за 100мс, а может, и быстрее).

        • »
          »
          »
          »
          »
          12 лет назад, # ^ |
            Проголосовать: нравится 0 Проголосовать: не нравится

          Даже 100мс — это практически бесконечно большой оверхед для одного теста, как мне кажется.

          • »
            »
            »
            »
            »
            »
            12 лет назад, # ^ |
              Проголосовать: нравится 0 Проголосовать: не нравится

            Ну обычно оверхед не сильно меньше, ~10мс. Тем более, никто не мешает создавать один контейнер на всё тестирование одной программы, а оверхед на запуск программы в уже созданном контейнере там совсем маленький (единственная проблема — программа может похакать контейнер и сохранить какие-то промежуточные данные между запусками на разных тестах, но эта угроза не такая уж и страшная, имхо).

            • »
              »
              »
              »
              »
              »
              »
              12 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится

              WAT? 10мс не сильно меньше 100мс? Разница — 10 раз. Запуск 30 решений на 70 тестах будет работать на три минуты быстрее.

              • »
                »
                »
                »
                »
                »
                »
                »
                12 лет назад, # ^ |
                  Проголосовать: нравится 0 Проголосовать: не нравится

                Поправка — на 3 минуты быстрее, если всё тестируется на одном компе. И если создаётся новый контейнер на каждый запуск, что делать не обязательно, как я написал выше. А если создавать контейнер на решение — то на 30 решений задержка будет всего 3 секунды, что мелочи.

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  12 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  Например, на школьных сборах обычно всё тестируется на одном компьютере. А при наличии Feedback любые оверхеды сильно бесят.

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

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  12 лет назад, # ^ |
                    Проголосовать: нравится +16 Проголосовать: не нравится

                  При том, как сейчас запускают решения, проблемы с безопасностью горааздо больше. Если решение сможет повысить свои права внутри контейнера, то оно испортит только свой контейнер; а если в системе, в которой все остальные решения запускаются и работает тестирующая система — то это уже гораздо печальней.

      • »
        »
        »
        »
        12 лет назад, # ^ |
        Rev. 2   Проголосовать: нравится +15 Проголосовать: не нравится

        На правах флейма. Код в Java выполняется внутри виртуальной машины JVM. Ее можно поместить в песочницу, то есть, например, еще в одну виртуальную машину. И все это запускать на виртуальном сервере. Таким образом получаем три уровня виртуализации :)

        We need to get deeper.

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Кстати, запускать сабмиты на виртуалке, в которой остановлены часы, — это отличная идея борьбы с рандомом в решениях :)

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится +9 Проголосовать: не нравится

        Это отличная идея для борьбы с отсечением по времени. По-моему, это уже очень серьезный перебор.

        • »
          »
          »
          »
          »
          12 лет назад, # ^ |
            Проголосовать: нравится +3 Проголосовать: не нравится

          Серьезный перебор во всех смыслах.

          • »
            »
            »
            »
            »
            »
            12 лет назад, # ^ |
              Проголосовать: нравится 0 Проголосовать: не нравится

            Нет, ну некоторые так, шутя, пишут перебор с отсечением по времени, а когда приходится писать перебор нормальный — они становятся серьезными как Serious Cat.

            По теме: лично мне не смешно.

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        Ну зачем останавливать время? Это в стиле "при линковке решения подсунуть вместо rand() функцию, возвращающую 0". Можно стартовое время одинаковое ставить, но от рандома это не спасёт, т.к. даже у времени выполнения программы есть некоторая дисперсия. (и по-хорошему, запрещать читать содержимое /dev/random тоже не стоит; хотя таким путём ТЛ легко словить).

        А зачем вообще бороться с рандомом? Если решения падают при реджадже — то участники ССЗБ, раз у них вероятность неудачи такая высокая.

        • »
          »
          »
          »
          »
          12 лет назад, # ^ |
            Проголосовать: нравится +5 Проголосовать: не нравится

          Понятно, что такой ерундой как остановка часов никто заниматься не собирается. Но с рандомом, тем не менее, нужно как-то бороться. Согласитесь, что ненормально, когда результат проверки зависит от момента запуска решения. Решения проверяются автоматически, но жюри соревнований при этом может вмешиваться в этот процесс (например, останавливая очередь проверки), влияя тем самым на результаты проверки недетерминированных решений. Чтобы никто не мог обвинить жюри в предвзятости, нужно четкое правило, регулирующее проверку таких решений. Хорошим выходом из ситуации будет правило, разрешающее запускать решение на одном и том же тесте несколько раз. В качестве результата проверки в таком случае будет выбираться худший результат из всех. Неудивительно, что такое правильно уже есть на некоторых соревнованиях.

          Поэтому, если решение упало на реджадже из-за неудачного момента времени, то да, автор этого кода ССЗБ. Всегда пишите в начале решения srand(ваше-любимое-число) — и будет всем счастье!

          • »
            »
            »
            »
            »
            »
            12 лет назад, # ^ |
              Проголосовать: нравится +8 Проголосовать: не нравится

            По факту обычно наоборот — выбирают лучший результат. На TopCoder, например, в случае ТЛ на тесте его запускают второй раз. Ну и от случайности до конца все равно не избавится — например, 2 запуска вполне детерминированного алгоритма обычно дают немного разное время работы, а если это время как раз рядом с ТЛ...

»
12 лет назад, # |
  Проголосовать: нравится -20 Проголосовать: не нравится

Спасибо за решение проблемы. Хотя для меня это, по идее, не такая уж и проблема (алгоритм Миллера-Рабина писать умею), но... все равно еще раз спасибо :).

»
12 лет назад, # |
  Проголосовать: нравится +11 Проголосовать: не нравится

Самое смешное в этой истории с BigInteger.isProbablePrime то, что метод стал производить подозрительные действия только начиная с версии Java 6. Интересно, что сподвигло разработчиков JDK на такой странный рефакторинг?

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится +20 Проголосовать: не нравится

    Использовать SecureRandom вместо random — очень разумно, особенно в isProbablePrime(). Понятно, что эту функцию "в реальной жизни" чаще всего используют для криптографических нужд, а там обычный random недопустим.

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится +8 Проголосовать: не нравится

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

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится +8 Проголосовать: не нравится

        Это конечно верно, но менять сигнатуру метода было нельзя из соображений обратной совместимости, так что использовать внутри SecureRandom — лучшее, что можно было сделать, чтобы уберечь людей от security bug'ов.

»
12 лет назад, # |
  Проголосовать: нравится +2 Проголосовать: не нравится

Nice to hear that administration solves such a problems. Thank you. But, can you say what was your changes to SecureRandom in details?

Thanks.