From bb847847ae7f5c909d74e083fdfd6eb3082768e8 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 28 Jul 2026 12:53:31 +0900 Subject: [PATCH] Enable long path names on Windows without the registry value Windows applies the `longPathAware` manifest setting only when `LongPathsEnabled` is set in the registry, so paths longer than 260 characters still fail on a stock machine. Set the undocumented `IsLongPathAwareProcess` bit in the PEB at startup instead, which is what `RtlAreLongPathsEnabled` reads and what Go's runtime does since 1.23. [Bug #18947] [Bug #18923] Co-Authored-By: Claude Opus 5 --- NEWS.md | 6 ++++++ test/ruby/test_dir.rb | 16 ++++++++++++++++ win32/win32.c | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/NEWS.md b/NEWS.md index ca413c616f6..6925455351b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -244,6 +244,11 @@ Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for det * SunOS 4 (Solaris, i.e. SunOS 5, is unaffected) * BSD/OS (BSDi) +* Windows 10 1703 or later no longer needs the `LongPathsEnabled` registry + value to use paths longer than 260 characters. Each path component is still + limited to 255 characters, and a child process still starts with the + `MAX_PATH` limited current directory. [[Bug #18947]] + ## Compatibility issues * `Kernel#at_exit` and `END {}` now raise `Ractor::IsolationError` when called @@ -327,6 +332,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. ## JIT +[Bug #18947]: https://bugs.ruby-lang.org/issues/18947 [Feature #8948]: https://bugs.ruby-lang.org/issues/8948 [Feature #9779]: https://bugs.ruby-lang.org/issues/9779 [Feature #15330]: https://bugs.ruby-lang.org/issues/15330 diff --git a/test/ruby/test_dir.rb b/test/ruby/test_dir.rb index edb5210af16..2ac8b1dc138 100644 --- a/test/ruby/test_dir.rb +++ b/test/ruby/test_dir.rb @@ -590,6 +590,22 @@ class TestDir < Test::Unit::TestCase assert_empty(entries - Dir.glob("#{wild}/Common*", File::FNM_SHORTNAME), bug10819) end + def test_glob_long_path + bug18923 = '[Bug #18923]' + # the whole path is longer than MAX_PATH, while each component is + # within the 255 chars limit of NTFS + deep = File.join(@root, "a" * 200, "b" * 200) + begin + FileUtils.mkdir_p(deep) + rescue SystemCallError + omit "long path names are not available" + end + file = File.join(deep, "c.txt") + File.write(file, "") + assert_equal([file], Dir.glob(File.join(@root, "**", "*.txt")), bug18923) + assert_equal(["c.txt"], Dir.children(deep), bug18923) + end + def test_home_windows setup_envs(%w[HOME USERPROFILE HOMEDRIVE HOMEPATH]) diff --git a/win32/win32.c b/win32/win32.c index 4f250478146..62a296da64f 100644 --- a/win32/win32.c +++ b/win32/win32.c @@ -843,6 +843,44 @@ static int w32_cmdvector(const WCHAR *, char ***, UINT, rb_encoding *); // // Initialization stuff // + +/* License: Ruby's */ +/* Enable long path names for this process, whatever the registry value + * HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled is. The + * loader sets the same PEB bit for longPathAware in the manifest, but only + * when that registry value is set, so the manifest alone is not enough on a + * stock machine. Undocumented, and taken from initLongPathSupport in Go's + * runtime , whose maintainers have + * proposed to drop it . */ +static void +init_long_path_support(void) +{ + /* PEB.BitField, and the IsLongPathAwareProcess bit in it */ + enum {peb_bit_field_offset = 3, is_long_path_aware_process = 0x80}; + typedef long (WINAPI version_func)(OSVERSIONINFOW *); + typedef void *(WINAPI peb_func)(void); + version_func *pRtlGetVersion; + peb_func *pRtlGetCurrentPeb; + OSVERSIONINFOW osvi; + BYTE *bit_field; + + pRtlGetVersion = (version_func *)get_proc_address("ntdll.dll", "RtlGetVersion", NULL); + if (!pRtlGetVersion) return; + memset(&osvi, 0, sizeof(osvi)); + osvi.dwOSVersionInfoSize = sizeof(osvi); + if (pRtlGetVersion(&osvi)) return; + + /* the PEB bit is honored since Windows 10 1703 (10.0.15063) */ + if (osvi.dwMajorVersion < 10) return; + if (osvi.dwMajorVersion == 10 && osvi.dwMinorVersion == 0 && + osvi.dwBuildNumber < 15063) return; + + pRtlGetCurrentPeb = (peb_func *)get_proc_address("ntdll.dll", "RtlGetCurrentPeb", NULL); + if (!pRtlGetCurrentPeb) return; + bit_field = (BYTE *)pRtlGetCurrentPeb() + peb_bit_field_offset; + *bit_field |= is_long_path_aware_process; +} + /* License: Ruby's */ void rb_w32_sysinit(int *argc, char ***argv) @@ -854,6 +892,7 @@ rb_w32_sysinit(int *argc, char ***argv) SetErrorMode(SEM_FAILCRITICALERRORS|SEM_NOGPFAULTERRORBOX); get_version(); + init_long_path_support(); // // subvert cmd.exe's feeble attempt at command line parsing