Вы могли заметить, что в Windows строгая проверка размеров записей является обычным делом.
Например, рассмотрим запись TMenuItemInfo (*):
type tagMENUITEMINFOA = record cbSize: UINT; fMask: UINT; fType: UINT; // используется, если MIIM_TYPE (4.0) или MIIM_FTYPE (>4.0) fState: UINT; // используется, если MIIM_STATE wID: UINT; // используется, если MIIM_ID hSubMenu: HMENU; // используется, если MIIM_SUBMENU hbmpChecked: HBITMAP; // используется, если MIIM_CHECKMARKS hbmpUnchecked: HBITMAP; // используется, если MIIM_CHECKMARKS dwItemData: ULONG_PTR; // используется, если MIIM_DATA dwTypeData: LPSTR; // используется, если MIIM_TYPE (4.0) или MIIM_STRING (>4.0) cch: UINT; // используется, если MIIM_TYPE (4.0) или MIIM_STRING (>4.0) {$IFDEF WIN98ME_UP} hbmpItem: HBITMAP; // доступно только в Windows 2000 и выше {$ENDIF WIN98ME_UP} end;Заметьте, что размер этой структуры зависит от того, определена ли директива WIN98ME_UP (т.е., в зависимости от того, нацеливаетесь ли вы на Windows 2000 или выше). Если вы возьмёте версию записи для Windows 2000 и передадите её на Windows NT 4, то вызов функции будет неуспешным, потому что размер не совпадает с ожидаемым.
"Но старые версии операционных систем должны принимать любой размер, который больше или равен размеру, который они ожидают. Больший размер означает, что запись пришла от более новой версии программы, и ОС должна просто игнорировать части, которые она не понимает."Мы пробовали так. И это не стало работать.
Рассмотрим следующую воображаемую запись и функцию, в которую она передаётся. Мы используем их как подопытных кроликов для дальнейшего обсуждения:
type tagIMAGINARY = record cbSize: UINT; fDance: BOOL; // Танцевать fSing: BOOL; // Петь {$IFDEF IMAGINARY_VERSION_2_UP} // Новые фишки, добавленные в v2 psp: IServiceProvider; // Где искать больше информации {$ENDIF} end; TImaginary = tagIMAGINARY; // выполнить действия, которые мы указали procedure DoImaginaryThing(const pimg: TImaginary); stdcall; // запросить, какие действия сейчас выполняются procedure GetImaginaryThing(var pimg: TImaginary); stdcall;Сначала мы обнаружим, что куча программ вообще просто забывают инициализировать поле cbSize.
var img: TImaginary; ... img.fDance := True; img.fSing := False; DoImaginaryThing(img);Поэтому вместо размера записи у них там лежит мусор со стека. Мусор со стека часто бывает большими числами, поэтому он проходит проверку "больше или равен ожидаемому размеру cbSize" и код работает. Тогда новая версия заголовочного файла расширяет запись, используя cbSize для определения: использует ли вызывающий новую или старую версию. Теперь, мусор из стека всё ещё больше или равен новому cbSize, поэтому версия 2 функции DoImaginaryThing говорит: "О, клёво, тут кто-то хочет предоставить нам дополнительную информацию через поле IServiceProvider". За исключением, конечно, того, что там лежит мусор из стека, поэтому вызов метода IServiceProvider.QueryService приведёт к вылету.
Теперь рассмотрим и другой возможный сценарий:
var img: TImaginary; ... GetImaginaryThing(img);Новая версия заголовочного файла расширяет запись, а мусор в стеке по-прежнему большое число и проходит проверку "больше или равен ожидаемому размеру cbSize", поэтому функция возвращает не только поля fDance и fSing, но и psp. Упс, но ведь вызывающий был скомпилирован с записью версии v1, поэтому у его записи нет никакого поля psp. Поле psp записывается за концом записи, перезаписывая любые данные, лежащие там. Ох, теперь у нас одна из этих ужасных проблем переполнения буфера.
Даже, если вам повезло и память после записи можно попортить, у вас всё ещё есть баг: по правилам учёта ссылок COM, когда функция возвращает интерфейс, вызывающая сторона ответственна за освобождение интерфейса, когда он более не нужен. Но вызывающая сторона работает с v1 и ничего не знает об этом поле psp, поэтому, конечно же, она не знает, что нужно вызывать psp := nil (в Delphi это приводит к psp._Release). Поэтому теперь в дополнении к порче памяти (как будто одного этого нам было недостаточно), у вас также появляется утечка памяти.
Постойте, я ещё не закончил. Теперь давайте посмотрим, что произойдёт, если новая версия программы работает на старой системе.
Предположим, что кто-то пишет программу, предназначенную для работы на системах с версией v2. Он устанавливает cbSize равный большему размеру записи v2 и устанавливает поле psp на поставщика услуг, который производит проверку безопасности перед тем, как разрешать кому-то танцевать или петь (к примеру, проверяет, что все заплатили за вход). Теперь другой человек берёт эту программу и запускает на системе версии v1. Размер записи нового формата v2, конечно же, проходит проверку "больше или равен размеру записи v1", поэтому система v1 примет эту запись и "Выполнит Воображаемое Действие" ("Do the Imaginary Thing"). За исключением того, что v1 не поддерживает поле psp, поэтому ваш провайдер ни разу не будет вызван и вся ваша система безопасности окажется не у дел. Теперь все ходят в ваш клуб, не платя входную плату.
Теперь, вы можете сказать: "ну, это всё багнутые программы. Это их вина". Если вы придерживаетесь такой логики, тогда не удивляйтесь, когда как грибы после дождя начнут появляться журнальные статьи вроде "Microsoft намеренно сделала <Продукт X> несовместимым с <программа от конкурента>. Где же министерство юстиции (Justice Department), когда оно так нужно?".
Примечание переводчика: (*) определение структуры взято из заголовочников JEDI. В самой Delphi обычно участвует всего один вариант структуры (он может быть разным в зависимости от версии Delphi).
Комментариев нет:
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.