Как многие знают, вызов 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.
Кстати, вопрос знающим людям. Как подобные проблемы решаются в других проверяющих системах, в частности, на NEERC'е?
Вроде бы можно выставить свойство java.security.egd чтобы указать, откуда брать энтропию
Не знаю, как эта проблема решается в PCMS2 и Ejudge, но пытаться запрещать системные вызовы — какая-то злая идея. Гораздо разумнее подсунуть программе фейковое окружение, и пусть получает текущее время или ip-адрес сколько душе угодно.
Для управления разрешениями в Java есть встроенный механизм. Системные вызовы в данном случае оказались запрещены случайно. Но, как написал Stigius, снятие этих ограничений не решило проблему — Restricted function сменился на Time limit exceeded.
На правах флейма. Код в Java выполняется внутри виртуальной машины JVM. Ее можно поместить в песочницу, то есть, например, еще в одну виртуальную машину. И все это запускать на виртуальном сервере. Таким образом получаем три уровня виртуализации :)
Ну т.е. для каждого языка у вас какие-то свои средства обеспечения безопасности? Выглядит костыльно. Давно уже существуют механизмы виртуализации, достаточно быстрые для использования при тестировании (см. например LXC; инициализировать новый контейнер там можно за 100мс, а может, и быстрее).
Даже 100мс — это практически бесконечно большой оверхед для одного теста, как мне кажется.
Ну обычно оверхед не сильно меньше, ~10мс. Тем более, никто не мешает создавать один контейнер на всё тестирование одной программы, а оверхед на запуск программы в уже созданном контейнере там совсем маленький (единственная проблема — программа может похакать контейнер и сохранить какие-то промежуточные данные между запусками на разных тестах, но эта угроза не такая уж и страшная, имхо).
WAT? 10мс не сильно меньше 100мс? Разница — 10 раз. Запуск 30 решений на 70 тестах будет работать на три минуты быстрее.
Поправка — на 3 минуты быстрее, если всё тестируется на одном компе. И если создаётся новый контейнер на каждый запуск, что делать не обязательно, как я написал выше. А если создавать контейнер на решение — то на 30 решений задержка будет всего 3 секунды, что мелочи.
Например, на школьных сборах обычно всё тестируется на одном компьютере. А при наличии Feedback любые оверхеды сильно бесят.
Не обязательно, но тогда, опять же, теоретические проблемы с безопасностью. Например, решение может сохранять, получать и использовать номер теста.
При том, как сейчас запускают решения, проблемы с безопасностью горааздо больше. Если решение сможет повысить свои права внутри контейнера, то оно испортит только свой контейнер; а если в системе, в которой все остальные решения запускаются и работает тестирующая система — то это уже гораздо печальней.
We need to get deeper.
Кстати, запускать сабмиты на виртуалке, в которой остановлены часы, — это отличная идея борьбы с рандомом в решениях :)
Это отличная идея для борьбы с отсечением по времени. По-моему, это уже очень серьезный перебор.
Серьезный перебор во всех смыслах.
Нет, ну некоторые так, шутя, пишут перебор с отсечением по времени, а когда приходится писать перебор нормальный — они становятся серьезными как Serious Cat.
По теме: лично мне не смешно.
Ну зачем останавливать время? Это в стиле "при линковке решения подсунуть вместо rand() функцию, возвращающую 0". Можно стартовое время одинаковое ставить, но от рандома это не спасёт, т.к. даже у времени выполнения программы есть некоторая дисперсия. (и по-хорошему, запрещать читать содержимое /dev/random тоже не стоит; хотя таким путём ТЛ легко словить).
А зачем вообще бороться с рандомом? Если решения падают при реджадже — то участники ССЗБ, раз у них вероятность неудачи такая высокая.
Понятно, что такой ерундой как остановка часов никто заниматься не собирается. Но с рандомом, тем не менее, нужно как-то бороться. Согласитесь, что ненормально, когда результат проверки зависит от момента запуска решения. Решения проверяются автоматически, но жюри соревнований при этом может вмешиваться в этот процесс (например, останавливая очередь проверки), влияя тем самым на результаты проверки недетерминированных решений. Чтобы никто не мог обвинить жюри в предвзятости, нужно четкое правило, регулирующее проверку таких решений. Хорошим выходом из ситуации будет правило, разрешающее запускать решение на одном и том же тесте несколько раз. В качестве результата проверки в таком случае будет выбираться худший результат из всех. Неудивительно, что такое правильно уже есть на некоторых соревнованиях.
Поэтому, если решение упало на реджадже из-за неудачного момента времени, то да, автор этого кода ССЗБ. Всегда пишите в начале решения srand(ваше-любимое-число) — и будет всем счастье!
По факту обычно наоборот — выбирают лучший результат. На TopCoder, например, в случае ТЛ на тесте его запускают второй раз. Ну и от случайности до конца все равно не избавится — например, 2 запуска вполне детерминированного алгоритма обычно дают немного разное время работы, а если это время как раз рядом с ТЛ...
Спасибо за решение проблемы. Хотя для меня это, по идее, не такая уж и проблема (алгоритм Миллера-Рабина писать умею), но... все равно еще раз спасибо :).
Самое смешное в этой истории с BigInteger.isProbablePrime то, что метод стал производить подозрительные действия только начиная с версии Java 6. Интересно, что сподвигло разработчиков JDK на такой странный рефакторинг?
Использовать SecureRandom вместо random — очень разумно, особенно в isProbablePrime(). Понятно, что эту функцию "в реальной жизни" чаще всего используют для криптографических нужд, а там обычный random недопустим.
При криптографическом использовании лично мне захотелось бы явно передать в этот метод специально созданный генератор, а не довольствоваться неким зашитым в нее намертво пусть даже и хорошим, но неизвестным генератором.
Это конечно верно, но менять сигнатуру метода было нельзя из соображений обратной совместимости, так что использовать внутри SecureRandom — лучшее, что можно было сделать, чтобы уберечь людей от security bug'ов.
Nice to hear that administration solves such a problems. Thank you. But, can you say what was your changes to SecureRandom in details?
Thanks.