ANSI実装における _splitpath() / _splitpath_s() の罠

2017/01/21


IPA セキュア・プログラミング講座 Windowsパス名の落とし穴にも載っていないが、とても深刻な話。

たとえば(長いので改行可としていますが実際はひとつの文字列です)

D:\a\bbbbbbbbb\splitpath.001longfn.002longfn.003longfn.004longfn.005longfn.006longfn.007longfn.008longfn.009longfn.010longfn.011longfn.012longfn.013longfn.014longfn.015longfn.016longfn.017longfn.018longfn.019longfn.020longfn.021longfn.022longfn.023longfn..exe
というファイル名は、Windows NT系では(FATであっても)実際に存在できます。

こいつを _splitpath_s 関数で解析しようとして…あれれ、ドライブレターから拡張子まで259文字、最後のヌル文字を加えて260文字あります。単位が「バイト」じゃなくて「文字」ですね。

Win32のANSI実装の場合、このファイル名文字列の全長は最後のヌル文字込みで500バイトあります。一方、たとえば _MAX_FNAME は大抵256です。

…あっさりと Buffer Overrun だ…

ということで、MSDNのANSI実装のサンプル https://msdn.microsoft.com/ja-jp/library/5a1745t0.aspx は、ちょいとまずいようです。セキュリティ強化バージョン関数 _splitpath_s なら処理が中断される以外の実害は一応ないと思われるが、OSが普通に扱えるファイルが正しく扱われないのはプログラムの製品品質として問題であるし、 _splitpath_s が処理中断した後を正しく跡始末できるかどうかはまさにプログラマの責任です。これは非常に厄介な問題です。

そして、古いアンセキュア版 _splitpath では、単にこのような名前のファイルが存在するだけで非常にやばいことになります。

これはANSI実装固有の問題です。更に _tsplitath_s でANSI/Unicode自動切換えさせるときが最も問題で、条件コンパイルするしかないようです。

MSDN のサンプルに基づく不味い実装
#include <stdlib.h>

int main( void )
{
   char path_buffer[_MAX_PATH];
   char drive[_MAX_DRIVE];
   char dir[_MAX_DIR];
   char fname[_MAX_FNAME];
   char ext[_MAX_EXT];
   errno_t err;
   
   ...
   
   err = _splitpath_s( path_buffer, drive, _MAX_DRIVE, dir, _MAX_DIR, fname, _MAX_FNAME, ext, _MAX_EXT );  
   ...
}
修正
(ANSI専用)
#include <stdlib.h>

int main( void )
{
   char path_buffer[_MAX_PATH * 2];
   char drive[_MAX_DRIVE];
   char dir[_MAX_DIR * 2];
   char fname[_MAX_FNAME * 2];
   char ext[_MAX_EXT * 2];
   errno_t err;
   
   ...
   
   err = _splitpath_s( path_buffer, drive, _MAX_DRIVE, dir, _MAX_DIR * 2, fname, _MAX_FNAME * 2, ext, _MAX_EXT * 2 );  
   ...
}
いちおう ANSI/Unicode 両用
(Unicode 実装の UNC path 32767 文字ファイル名未対応)
#include <stdlib.h>
#include <tchar.h>
#define _T_MAX_PATH  (_MAX_PATH  * ( sizeof(wchar_t) / sizeof(_TCHAR) ) )
#define _T_MAX_DRIVE (_MAX_DRIVE)
#define _T_MAX_DIR   (_MAX_DIR   * ( sizeof(wchar_t) / sizeof(_TCHAR) ) )
#define _T_MAX_FNAME (_MAX_FNAME * ( sizeof(wchar_t) / sizeof(_TCHAR) ) )
#define _T_MAX_EXT   (_MAX_EXT   * ( sizeof(wchar_t) / sizeof(_TCHAR) ) )

int _tmain( void )
{
   _TCHAR path_buffer[_T_MAX_PATH];
   _TCHAR drive[_T_MAX_DRIVE];
   _TCHAR dir[_T_MAX_DIR];
   _TCHAR fname[_T_MAX_FNAME];
   _TCHAR ext[_T_MAX_EXT];
   errno_t err;
   
   ...
   
   err = _tsplitpath_s( path_buffer, drive, _T_MAX_DRIVE, dir, _T_MAX_DIR, fname, _T_MAX_FNAME, ext, _T_MAX_EXT );  
   ...
}

「sizeof(wchar_t) を sizeof(_TCHAR) で割る」というのはすごくわかりにくい気もする。要は Unicode なら必要な「文字数」の _TCHAR を確保し、ANSI の場合は文字数の2倍に相当する「バイト数」の char を確保する、ということなんですが、自分でも久しぶりに見て混乱した。なんかもうちょっと、良い方法はないものか。
なお、ドライブレターは「A-Zの1文字」だけしか有り得ないので、ANSI でも Unicode でも _MAX_DRIVE のままで良い。が、たった1バイトをケチることがソースコードの可読性観点上混乱を招くという見方も否定できず、表記統一しても良い気がする。UNC パスを _tsplitpath_s() にかけた場合はドライブレターは空が返るので問題ない。

Unicode 実装の UNC path 32767 文字のファイル名に対応するためには、drive 以外のものをすべて 64KB 用意すればよいのだと思うが、試していない。https://docs.microsoft.com/ja-jp/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd

しかし、もしサロゲートペア文字列がファイル名に含まれている場合、一体どうなるんだろう?

また、_makepath_s はどう対応すればいいんだろうか、思いつかない。


略歴


トップページ / 計算機関係の雑多なこと / ANSI実装における _splitpath() / _splitpath_s() の罠
このサイトについて・著者連絡先