How Windows DLL Name Resolution Works - Search Order and SxS
· Updated: · Go Komura · Windows, DLL, Loader, Security, Windows Development
Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614574)
- First published
Cite this article(DOI: 10.5281/zenodo.21614573)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). How Windows DLL Name Resolution Works - Search Order and SxS. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614573 https://comcomponent.com/en/blog/2026/03/24/002-windows-dll-name-resolution/
- DOI (latest version)
- 10.5281/zenodo.21614573
- DOI (this version)
- 10.5281/zenodo.22218892
Whenever native DLLs on Windows come up, confusion like the following arises with remarkable frequency.
- When you write
LoadLibrary("foo.dll"), where does it actually look? - You put the DLL in the same folder as the executable, so why is a different DLL being loaded?
- Does
System32take precedence, or does the application folder? - At which stage do manifests, API sets, and Known DLLs take effect?
- What changes when you use
SetDllDirectoryorAddDllDirectory? - What makes DLL planting attacks and DLL hijacking likely to happen?
Memorizing the search order as a one-liner is not enough for real work. In reality, the Windows loader evaluates several special rules before it ever starts walking the file system in order.
flowchart TB
accTitle: Why memorizing the search order is not enough
accDescr: A diagram showing that memorizing the DLL search order as a single line does not help in real work, because the Windows loader evaluates several special rules before it walks the file system in order.
a0["Memorize the search order as one line"] -.-> a1["Does not help in real work"]
a2["What the Windows loader really does"] --> a3["Evaluate the special rules first"]
a3 --> a4["Then walk the file system in order"]
Figure 1: Name resolution begins with pre-stage special rules, before any folder search.
In this article, we organize DLL name resolution on Windows for practical use, covering the differences between unpackaged and packaged apps, Known DLLs, the loaded-module list, API sets, side-by-side manifests, and the effects of the LoadLibraryEx family of APIs.
The content is based on public information on Microsoft Learn as of March 2026.123456789
Terms used in this article
Here is a one-line summary of each term that appears below without further introduction. The details are covered in the body of the article.
| Term | In one line |
|---|---|
| packaged app / unpackaged app | Whether the app is distributed and installed as a package such as MSIX, or ships the traditional way with an executable placed in a folder. The search order is defined separately for each (sections 3 and 4) |
| safe DLL search mode | A setting that is enabled by default and moves the current folder toward the back of the search order. Setting the registry value SafeDllSearchMode to 0 disables it1 |
| DLL redirection | Placing a marker named AppName.exe.local next to the executable makes the loader look in the executable’s folder first. Also called .local or DotLocal (section 7)7 |
| SxS (side-by-side) | A mechanism for letting multiple versions of the same DLL coexist, with a manifest declaring which version to bind to. SxS is the abbreviation of side-by-side (section 7)9 |
| loaded-module list | The mechanism by which the system checks whether a DLL with the same module name is already loaded into that process’s memory. If it is, that module is used regardless of which folder it came from (5.1)1 |
| Known DLLs | The list of DLLs Windows treats as well known for that version. For a DLL on the list, the system-side copy is used (5.2)1 |
| API set | A contract name such as api-ms-win-.... It is a virtual alias that hides the physical DLL providing the implementation (section 6)3 |
| package dependency graph | The app’s own package plus the dependency packages declared as PackageDependency entries in the Dependencies section of the package manifest. They are searched in the order written in the manifest1 |
1. The Conclusion First
Here are the practical conclusions up front.
- Windows DLL name resolution is not “file system search first.” Elements like DLL redirection, API sets, SxS manifests, the loaded-module list, and Known DLLs come before the search order proper.1
- In the standard configuration for an unpackaged app with safe DLL search mode enabled, the application folder ranks high, but the special rules above are evaluated before it.1
- Even if you load a DLL by full path, its dependent DLLs are not automatically pinned to the same full path. Dependent DLLs are searched by module name only, so they can be resolved from somewhere else.1
Known DLLsis a mechanism by which the OS binds certain well-known DLLs to the system-side copies; it is not something a normal app-side deployment overrides.1- An API set is not “the actual DLL name itself” but a virtual alias that hides the implementation DLL. If you see a name like
api-ms-win-...and reason about it the way you would about a normal DLL search, you will likely misunderstand.3 SetDllDirectorydoes more than change the search order - it has the behavior of effectively disabling safe DLL search mode, so using it casually can backfire from a security standpoint.1- In practice, the safe approach is to combine full-path loading,
SetDefaultDllDirectories,AddDllDirectory, and theLOAD_LIBRARY_SEARCH_*flags ofLoadLibraryExto narrow the search scope explicitly.4562
In short, the practical view is that Windows DLL name resolution is determined not just by “which folder comes in what order,” but by “which pre-stage rules resolve the name first” and “how the APIs have altered the search space.”
flowchart TB
accTitle: The three things that decide DLL name resolution
accDescr: A diagram showing that Windows DLL name resolution is decided by the overlap of three things, not folder order alone: the pre-stage rules that decide what the name resolves to, the folder search order, and the search space as altered by API calls.
b1["Pre-stage rules for the name"] --> b4["The DLL actually loaded"]
b2["Folder search order"] --> b4
b3["Search space altered by APIs"] --> b4
b1 -.-> b1d["redirection and Known DLLs"]
b3 -.-> b3d["LoadLibraryEx flags"]
Figure 2: The result comes from the overlap of pre-stage rules, folder order, and the APIs.
Knowledge map for this article
This article explains that DLL name resolution on Windows is not merely an order in which folders are searched, but a mechanism in which the earlier rules of DLL redirection, API sets, the SxS manifest, the loaded-module list, and Known DLLs are evaluated before the file system search. The search order itself is defined differently for a packaged app and an unpackaged app, and the position of the current folder changes depending on whether safe DLL search mode is enabled or disabled. Because SetDllDirectory effectively disables safe DLL search mode, combining SetDefaultDllDirectories, the LoadLibraryEx search flags, and AddDllDirectory to narrow the search space is what mitigates DLL hijacking. Specifying a full path does not automatically pin the dependent DLLs as well, so check where a module was actually loaded from with Process Monitor or ListDLLs.
flowchart LR
accTitle: Windows DLL name resolution
accDescr: Diagram showing that DLL redirection, API sets, the SxS manifest, the loaded-module list, Known DLLs, and the package dependency graph are evaluated ahead of the file system search, that the search order itself is defined differently for a packaged app and an unpackaged app, that SetDllDirectory weakens safe DLL search mode while SetDefaultDllDirectories and the LoadLibraryEx search flags narrow the search space and mitigate DLL hijacking, and how all of this relates to the means of confirming the actual load source with tools such as Process Monitor
dll_search_order["DLL Search Order"]
dll_hijacking["DLL Hijacking (Binary Planting)"]
dll_redirection["DLL Redirection (.local)"]
api_set["Windows API Set"]
sxs_manifest_redirection["SxS Manifest Redirection"]
loaded_module_list["Loaded-Module List"]
known_dlls["Known DLLs"]
package_dependency_graph["Package Dependency Graph"]
current_folder_search["Current Directory Search Step"]
safe_dll_search_mode["Safe DLL Search Mode"]
packaged_app["Packaged App"]
unpackaged_app["Unpackaged App"]
setdlldirectory["SetDllDirectory"]
setdefaultdlldirectories["SetDefaultDllDirectories"]
adddlldirectory["AddDllDirectory"]
loadlibraryex["LoadLibraryEx"]
dll_search_path_hardening["Narrowing the DLL Search Path"]
bare_name_loadlibrary["LoadLibrary Without a Full Path"]
fullpath_load["Full-Path DLL Loading"]
dependent_dll_resolution["Dependent DLL Resolution"]
dllimportsearchpath["DllImportSearchPath (.NET)"]
procmon["Process Monitor (procmon.exe)"]
listdlls_tool["ListDLLs (Sysinternals)"]
tasklist_command["tasklist /m"]
dll_search_order -->|"uses"| dll_redirection
dll_search_order -->|"uses"| api_set
dll_search_order -->|"uses"| sxs_manifest_redirection
dll_search_order -->|"uses"| loaded_module_list
dll_search_order -->|"uses"| known_dlls
dll_search_order -.->|"uses"| package_dependency_graph
current_folder_search -->|"configured by"| safe_dll_search_mode
packaged_app -->|"incompatible with"| unpackaged_app
packaged_app -->|"uses"| package_dependency_graph
setdlldirectory -->|"incompatible with"| safe_dll_search_mode
setdefaultdlldirectories -->|"mitigates"| dll_hijacking
adddlldirectory -.->|"requires"| setdefaultdlldirectories
loadlibraryex -->|"uses"| adddlldirectory
setdlldirectory -->|"not recommended for"| dll_search_path_hardening
loadlibraryex -->|"recommended for"| dll_search_path_hardening
dll_search_path_hardening -->|"mitigates"| dll_hijacking
bare_name_loadlibrary -->|"may cause"| dll_hijacking
fullpath_load -.->|"mitigates"| dll_hijacking
fullpath_load -->|"not recommended for"| dependent_dll_resolution
dllimportsearchpath -->|"uses"| loadlibraryex
dll_search_order -->|"verified by"| procmon
loaded_module_list -->|"verified by"| listdlls_tool
loaded_module_list -->|"verified by"| tasklist_command
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (23 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. DLL Name Resolution Has Pre-Stage Rules Before “Folder Search”
According to the DLL search order documentation on Microsoft Learn, when a DLL is loaded, the following elements are treated as part of the search order first.1
- DLL redirection
- API sets
- SxS manifest redirection
- The loaded-module list
- Known DLLs
Only after that does the loader proceed to the file system search across the app folder, System32, the Windows folder, PATH, and so on.1
If you miss this, it can feel “wrong” that something gets decided before the application folder - but as a description of the Windows loader, that is in fact the main path.
flowchart TD
A["A DLL name needs resolving"] --> B["DLL redirection"]
B --> C["API set"]
C --> D["SxS manifest redirection"]
D --> E["loaded-module list"]
E --> F["Known DLLs"]
F --> G["Search order on the file system"]
G --> H["The DLL actually loaded is determined"]
Figure 3: The pre-stage rules are evaluated in turn, and only when none of them match does the file system search begin.
3. The Standard Search Order for Unpackaged Apps
For an ordinary desktop app loading a DLL without a full path, Microsoft Learn describes the standard search order for unpackaged apps. In the default state with safe DLL search mode enabled, the order is as follows.1
| # | Where it looks | Kind | Notes |
|---|---|---|---|
| 1 | DLL redirection | Pre-stage rule | Whether an AppName.exe.local marker exists (section 7) |
| 2 | API sets | Pre-stage rule | From the contract name to the implementation DLL (section 6) |
| 3 | SxS manifest redirection | Pre-stage rule | Binding declared by a manifest (section 7) |
| 4 | loaded-module list | Pre-stage rule | Whether a module of the same name is already loaded (5.1) |
| 5 | Known DLLs | Pre-stage rule | For a well-known DLL, the system-side copy (5.2) |
| 6 | package dependency graph | Pre-stage rule | Windows 11 version 21H2 and later. Searched in the order written in the manifest |
| 7 | The folder the application was loaded from | File system | This is where the actual folder search starts |
| 8 | The system folder | File system | Normally %SystemRoot%\System32. The location returned by GetSystemDirectory |
| 9 | The 16-bit system folder | File system | The System folder from the 16-bit era. No function returns its path, but it is still searched. Modern apps essentially never have to think about it; knowing that it survives as an entry in the order is enough |
| 10 | The Windows folder | File system | The location returned by GetWindowsDirectory |
| 11 | The current folder | File system | With safe DLL search mode disabled, this moves up to position 8 |
| 12 | The directories listed in PATH |
File system | Per-application paths from the App Paths registry key are not included |
This table is not something to memorize. It is a table for working out which stage decided your particular case. If the outcome is decided at stages 1 through 6, no amount of rearranging folders at stage 7 and beyond will change anything.
flowchart TB
accTitle: How to use the search order table correctly
accDescr: A diagram showing that the search order table is not for memorizing but for working out which stage decided your case, and that trying to fix a problem decided by a pre-stage rule by rearranging folders changes nothing.
c1["Symptom: an unintended DLL is loaded"] --> c2{"Which stage decided it"}
c2 -->|"Pre-stage rules (1-6)"| c3["Moving folders around changes nothing"]
c2 -->|"File system (7-12)"| c4["Placement and paths are worth fixing"]
Figure 4: Before fixing anything, work out which stage decided the outcome.
Three points matter most in practice.
- The current folder is quite far down by default. Safe DLL search mode makes it hard to move the current folder forward.1
- However, being far down does not mean safe. As long as a directory the attacker can control remains in the search scope, room for DLL preloading remains.2
- On Windows 11 21H2 and later, the package dependency graph appears in the unpackaged app search description as well. This is an easy delta to miss if you only remember the older description.1
flowchart TB
accTitle: Where the current folder sits and what that means for safety
accDescr: A diagram showing that with safe DLL search mode enabled by default the current folder sits far down the search order, but that being far down does not make it safe, because room for DLL preloading remains as long as a directory the attacker can control is still in the search scope.
d1["safe DLL search mode enabled (default)"] --> d2["The current folder sits far down"]
d2 -.-> d3["Far down does not mean safe"]
d3 --> d4["Room remains if an attacker-controlled location is still searched"]
Figure 5: The current folder has been pushed down, but that alone is not a defense.
4. Packaged Apps and Unpackaged Apps Are Not the Same
Microsoft Learn defines a separate search order for packaged apps. In packaged apps, the package dependency graph takes effect at an earlier stage, and the search model itself is somewhat different.1
Miss this difference, and after moving to MSIX or adopting the Windows App SDK you get confusion like this:
- A DLL found when running unpackaged during development is not found in the production package
- Dependencies via the package manifest mix with old-style
PATHdependence, and the reproduction conditions change - Explaining “the Windows DLL search order is such-and-such” with a single table, dropping the behavioral differences of packaged apps
In articles and design reviews, it is safest to first separate “is this about a packaged app or an unpackaged app?”1
flowchart TB
accTitle: Separate packaged from unpackaged first
accDescr: A diagram showing that packaged apps have their own search order in which the package dependency graph takes effect earlier, that this causes confusion such as a DLL found during unpackaged development not being found in the production package, and that a design review should separate the two cases first.
e1{"Which kind of app is this about"}
e1 -->|"unpackaged app"| e2["The standard search order in section 3"]
e1 -->|"packaged app"| e3["A separate search order (dependency graph comes earlier)"]
e3 -.-> e4["A source of confusion once MSIX changes the behavior seen in development"]
Figure 6: The search order is not one table; it forks first on packaged versus unpackaged.
5. What Known DLLs and the Loaded-Module List Are Doing
The parts of DLL resolution most likely to defy intuition are the loaded-module list and Known DLLs.
5.1 The loaded-module list
Microsoft Learn explains that the system can check whether a DLL with the same module name is already loaded in memory.1
In other words, before the file system search, there is a check of
- whether that DLL name is already loaded, and
- consequently, whether there is any need to go searching at all.
So if, during an investigation, you drop the fact that “in this process, a same-named DLL from another folder was already loaded first,” you will misread the reproduction conditions.
flowchart TB
accTitle: What the loaded-module list decides
accDescr: A diagram showing that before the file system search the system checks whether a DLL with the same module name is already loaded into that process's memory, and that if it is, that module is used regardless of which folder it came from, so there is no need to go searching at all.
f1["A DLL name needs resolving"] --> f2{"Is a module of the same name already loaded"}
f2 -->|"Already loaded"| f3["Use that module (the folder is irrelevant)"]
f2 -->|"Not loaded"| f4["Continue with the rest of the search"]
f3 -.-> f5["Easy to get lost when a same-named DLL from another folder was loaded first"]
Figure 7: If a module of the same name is already loaded, nothing new is searched for.
5.2 Known DLLs
Known DLLs is the list of DLLs Windows considers well known for that version, and it can be inspected at HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs. For a DLL on the list, the system uses its copy of that known DLL.1
What matters here is that Known DLLs is not the kind of thing a regular app can “win” by placing a same-named DLL in its application folder.
Understanding it as a simple first-come race against System32 misreads the behavior.
flowchart TB
accTitle: What Known DLLs does
accDescr: A diagram showing that Known DLLs is the list of DLLs Windows treats as well known for that version, that the system-side copy is used for a DLL on the list, and that this is therefore not something a regular app can win by placing a same-named DLL in its application folder, so reading it as a first-come race against System32 misses the behavior.
g1["The DLL name to resolve"] --> g2{"Is it on the Known DLLs list"}
g2 -->|"On the list"| g3["The system-side copy is used"]
g2 -->|"Not on the list"| g4["Continue with the rest of the search"]
g3 -.-> g5["Not something an app folder can override"]
Figure 8: A known DLL binds to the system-side copy, so there is no race to win.
6. An API Set Is a Contract Name, Not an “Actual DLL Name”
When you see a name like api-ms-win-core-..., the reflex is to ask “where do I find that DLL file?” But Microsoft Learn explains that an API set is a virtual alias for a physical DLL - a mechanism that separates the contract from the implementation.3
In other words, reasoning like
- API set name = the physical DLL file name as is
- API set resolution = the same file search as a normal DLL
is inaccurate.
With the API set model in mind, it becomes easier to explain that
- things stay consistent even when the implementing DLL name differs across Windows versions and device types, and
- the caller does not need to know, in fixed terms, which host DLL provides the implementation.3
flowchart TB
accTitle: An API set is a contract name
accDescr: A diagram showing that a name such as api-ms-win- is not a physical DLL file name but a virtual alias that hides the implementation DLL, so that separating contract from implementation keeps things consistent even when the implementing DLL name differs across Windows versions and device types, and the caller never has to know which host DLL provides the implementation.
h1["A contract name such as api-ms-win-…"] --> h2["Resolved as a virtual alias"]
h2 --> h3["The implementing physical DLL stays hidden"]
h3 --> h4["Consistent even when the implementation differs by version or device"]
h1 -.-> h5["Treating it like a normal file search is inaccurate"]
Figure 9: An API set is not a file name to look for; it is a contract name that hides the implementation.
7. Manifests and Side-by-Side (SxS) Are an Alternative Answer to the DLL Versioning Problem
DLL redirection and SxS manifests are described not as minor search-order tricks but as mechanisms for avoiding DLL versioning conflicts.789
Microsoft Learn’s framing is:
- A manifest is XML describing a side-by-side assembly or an isolated application
- A side-by-side assembly is the unit of naming, binding, versioning, and deployment
- The loader decides which version to bind to based on the dependencies recorded in the manifest89
flowchart TB
accTitle: How manifests and side-by-side relate
accDescr: A diagram showing that a manifest is XML describing side-by-side assemblies and isolated applications, that a side-by-side assembly is the unit of naming, binding, versioning, and deployment, and that the loader decides which version to bind to from the dependencies recorded in the manifest, which is how DLL versioning conflicts are avoided.
i1["manifest (XML)"] --> i2["Declares the side-by-side assemblies and versions it depends on"]
i2 --> i3["The loader decides which version to bind to"]
i3 --> i4["Multiple versions of the same DLL can coexist"]
Figure 10: SxS is not a search-order trick; it is a separate answer to versioning conflicts.
So in practice you need to think about these three separately:
- Simply placing a private DLL in the app folder
- Using DLL redirection such as
.local - Using side-by-side binding via manifests
All of them are similar in that they affect DLL resolution, but their design intent is not the same. Microsoft Learn’s guidance is to pick between them this way: DLL redirection if you want to fix an existing application without touching it, side-by-side components if you are writing a new one.7
flowchart TB
accTitle: Choosing between the three mechanisms
accDescr: A diagram showing that placing a private DLL in the app folder, using DLL redirection such as .local, and using side-by-side binding via a manifest all affect DLL resolution but differ in design intent, and that the guidance is DLL redirection when fixing an existing application without touching it and side-by-side for a new application.
j0{"Which mechanism is this about"}
j0 --> j1["Put a private DLL in the app folder"]
j0 --> j2["DLL redirection (.local)"]
j0 --> j3["SxS manifest binding"]
j2 -.-> j4["When you need a fix without touching the existing app"]
j3 -.-> j5["Dependency management for a new app"]
Figure 11: The three mechanisms look alike; pick between them on design intent.
7.1 What .local Actually Does
This tends to get one line and no more, so here is the behavior for unpackaged apps, broken out.7
- What you put there: the redirection file is named
ExecutableName.local. ForEditor.exethat means placingEditor.exe.localin the same folder as the executable. The DLL you want loaded goes in that folder too - What goes in it: the contents of the file are ignored. Its mere existence is the signal that makes the loader look in the executable’s folder first when loading a DLL
- What it covers: it applies to full-path loads as well as module-name loads. Regardless of the path passed to
LoadLibraryorLoadLibraryEx, if a DLL of the same name sits in the executable’s folder, that one is loaded. The behavior exists to rescue situations like COM, where there is only one registration slot - When nothing is found: if the executable’s folder does not have it, the search falls back to the normal search order
- Folder form: it also works if you create a folder named
Editor.exe.localand put the DLLs inside it - Enabling it machine-wide: create a DWORD value named
DevOverrideEnableunderHKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options, set it to 1, and reboot. With this set,.localredirection takes effect even for apps that carry an application manifest - For packaged apps: the location changes - the loader looks in
<package install location>\microsoft.system.package.metadata\application.local\
There is one side effect here that will send an investigation off the rails if you miss it.
When DLL redirection is in use and the app cannot access every drive and directory in the search order, LoadLibrary stops searching at the point where access is denied. Without DLL redirection, inaccessible directories are skipped and the search continues.7
If a DLL that should be further down the order is reported missing only on machines where .local is present, suspect this difference.
flowchart TB
accTitle: How .local behaves
accDescr: A diagram showing that a marker named ExecutableName.local placed next to the executable acts as a signal regardless of its contents, making the executable's folder be looked at first even for full-path loads, that the search falls back to the normal order when nothing is found there, and that it carries the side effect of stopping the search on an access denial.
k1["Place ExecutableName.local"] --> k2["Look in the executable's folder first"]
k2 --> k3{"Is a DLL of the same name there"}
k3 -->|"Yes"| k4["That one is loaded even for a full path"]
k3 -->|"No"| k5["Fall back to the normal search order"]
k1 -.-> k6["Side effect: a denial ends the search"]
Figure 12: With .local, the marker’s existence is the signal, and it reaches even full-path loads.
8. What Changes with LoadLibraryEx, SetDllDirectory, and AddDllDirectory
8.1 SetDllDirectory
SetDllDirectory changes the search order, but Microsoft Learn explicitly states that it effectively disables safe DLL search mode.1
That is, even if you only intended to “add one app-specific folder,” the search space changes as a result, including how the current folder is treated.
Furthermore, calling SetDllDirectory in a parent process can affect the standard search order in child processes as well.1
For this reason, rather than using SetDllDirectory carelessly as a habit, it is safer in practice to lean on:
flowchart TB
accTitle: The trap in SetDllDirectory
accDescr: A diagram showing that SetDllDirectory, even when all you meant was to add one app-specific folder, effectively disables safe DLL search mode and changes the whole search space, that calling it in a parent process can affect the child process search order, and that leaning on SetDefaultDllDirectories, AddDllDirectory, and the LoadLibraryEx search flags is safer.
l1["Add one folder with SetDllDirectory"] --> l2["safe DLL search mode is effectively disabled"]
l2 --> l3["The whole search space changes, current folder included"]
l2 -.-> l4["Child process search order can be affected too"]
l3 --> l5["Lean on the SetDefaultDllDirectories family instead"]
Figure 13: What was meant as adding one folder weakens the entire search space.
8.2 AddDllDirectory
Paths added with AddDllDirectory are used in combination with LOAD_LIBRARY_SEARCH_USER_DIRS.
On Microsoft Learn, the search order among multiple added directories is unspecified.15
So a design that
- adds multiple directories, and
- strictly expects a particular search order among them
is best avoided.
flowchart TB
accTitle: Using AddDllDirectory and what to watch for
accDescr: A diagram showing that paths added with AddDllDirectory are used together with LOAD_LIBRARY_SEARCH_USER_DIRS, that the search order among multiple added directories is unspecified, and that a design which adds several directories and strictly expects a particular order among them should be avoided.
m1["Add a path with AddDllDirectory"] --> m2["Takes effect together with LOAD_LIBRARY_SEARCH_USER_DIRS"]
m2 -.-> m3["Order among multiple additions is unspecified"]
m3 --> m4["Avoid designs that depend on that order"]
Figure 14: The order among added user directories cannot be relied on by specification.
8.3 SetDefaultDllDirectories
SetDefaultDllDirectories is described as an API for removing the more vulnerability-prone directories from the standard DLL search path and restricting the search scope.4
Three properties are particularly worth knowing.
- It takes effect per process
- Once called, it persists for the lifetime of the process
- A standard search path once set cannot simply be restored to the original default
From a security standpoint, this API lends itself to a design of shifting to a safer search space right after startup.4
flowchart TB
accTitle: Properties of SetDefaultDllDirectories
accDescr: A diagram showing that SetDefaultDllDirectories removes the more vulnerability-prone directories from the standard DLL search path to restrict the search scope, that it takes effect per process and persists for the lifetime of the process, and that the search path once set cannot be restored to the original default, which makes shifting to a safer search space right after startup an easy design.
n1["Call SetDefaultDllDirectories right after startup"] --> n2["Drop the vulnerability-prone locations from the search scope"]
n2 --> n3["Takes effect per process for its whole lifetime"]
n3 -.-> n4["Cannot be restored to the original default"]
Figure 15: An API to call once at startup to move the whole process to the safe side.
8.4 LoadLibraryEx
LoadLibraryEx lets you change the search behavior with LOAD_WITH_ALTERED_SEARCH_PATH and the LOAD_LIBRARY_SEARCH_* flags.61
Practically, it is an API well suited to requirements like
- including the folder of the DLL being loaded in the search scope, dependencies included, or
- restricting the search to the application folder,
System32, and explicitly added user directories only.
There are four constraints worth knowing before you write any of it.6
| Constraint | Detail |
|---|---|
| Second argument | hFile is reserved for future use; always pass NULL |
| Cannot be combined | LOAD_WITH_ALTERED_SEARCH_PATH cannot be combined with any of the LOAD_LIBRARY_SEARCH_* flags |
| Full path required | If you use LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, the first argument must be a full path |
| Order when several are set | The search runs LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, then LOAD_LIBRARY_SEARCH_APPLICATION_DIR, then LOAD_LIBRARY_SEARCH_USER_DIRS, then LOAD_LIBRARY_SEARCH_SYSTEM32. The order within USER_DIRS is unspecified |
Specify even one LOAD_LIBRARY_SEARCH_* flag and the standard search path is not used at all.
So if you pass only LOAD_LIBRARY_SEARCH_SYSTEM32, the application folder is not searched. That is what narrowing means.
flowchart TB
accTitle: Specifying a search flag means the standard path is not used
accDescr: A diagram showing that specifying even one LOAD_LIBRARY_SEARCH_ flag means the standard search path is not used at all, so that passing only LOAD_LIBRARY_SEARCH_SYSTEM32 leaves the application folder unsearched, which is what narrowing the search means.
o1{"Was any LOAD_LIBRARY_SEARCH_ flag specified"}
o1 -->|"None specified"| o2["The standard search path is used"]
o1 -->|"At least one specified"| o3["The standard search path is not used at all"]
o3 --> o4["Only the scope of the specified flags is searched"]
o4 -.-> o5["SYSTEM32 alone leaves the app folder unsearched"]
Figure 16: The flags do not add to the search; they replace it with exactly that scope.
8.5 A Minimal Code Example (C/C++)
Putting the APIs so far into one place gives the following shape.
SetDefaultDllDirectories and the LOAD_LIBRARY_SEARCH_* flags are Windows 8 and later APIs, so the target version has to be stated explicitly for the headers.46
/* cl /W4 loader.c (Visual Studio 2022 + Windows SDK 10)
* SetDefaultDllDirectories / AddDllDirectory / LOAD_LIBRARY_SEARCH_* are
* Windows 8 and later. To also target Windows 7, KB2533623 is a prerequisite
* and you need to resolve them at run time from Kernel32.dll with
* GetProcAddress. */
#define _WIN32_WINNT 0x0602 /* Windows 8 */
#include <windows.h>
#include <stdio.h>
int wmain(void)
{
/* 1. Move the process-default search space to the safe side.
* The goal is to drop the current folder and PATH from the search scope.
* LOAD_LIBRARY_SEARCH_DEFAULT_DIRS is the combination of
* APPLICATION_DIR + SYSTEM32 + USER_DIRS.
* Without USER_DIRS in here, the AddDllDirectory call in step 2
* will not be reflected in the process default. */
if (!SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)) {
wprintf(L"SetDefaultDllDirectories failed: %lu\n", GetLastError());
return 1;
}
/* 2. Explicitly add just our own plugin folder.
* What you pass to AddDllDirectory must be an absolute path. */
const wchar_t *pluginDir = L"C:\\MyApp\\plugins";
DLL_DIRECTORY_COOKIE cookie = AddDllDirectory(pluginDir);
if (cookie == NULL) {
wprintf(L"AddDllDirectory failed: %lu\n", GetLastError());
return 1;
}
/* 3. Load it.
* The second argument hFile is reserved, so always NULL.
* Passing flags here means this combination of flags is used
* instead of the process default set up in step 1.
* APPLICATION_DIR is left out, so the app folder is not searched. */
HMODULE h = LoadLibraryExW(
L"foo.dll",
NULL,
LOAD_LIBRARY_SEARCH_USER_DIRS | LOAD_LIBRARY_SEARCH_SYSTEM32);
if (h == NULL) {
wprintf(L"LoadLibraryExW failed: %lu\n", GetLastError());
RemoveDllDirectory(cookie);
return 1;
}
/* 4. Always confirm where it was loaded from.
* In an investigation, where it loaded from matters more than
* whether it loaded. */
{
wchar_t loadedPath[MAX_PATH];
DWORD cap = (DWORD)(sizeof(loadedPath) / sizeof(loadedPath[0]));
DWORD len = GetModuleFileNameW(h, loadedPath, cap);
if (len > 0 && len < cap) {
wprintf(L"loaded from: %s\n", loadedPath);
} else {
wprintf(L"GetModuleFileNameW failed: %lu\n", GetLastError());
}
}
FreeLibrary(h);
RemoveDllDirectory(cookie);
return 0;
}
There are four things this code gets right.
- Call
SetDefaultDllDirectoriesfirst. Once called, it holds for the lifetime of the process, and you cannot revert to the standard search path4 AddDllDirectoryonly means something paired withLOAD_LIBRARY_SEARCH_USER_DIRS. IfSetDefaultDllDirectorieswas not givenUSER_DIRS, the added directory is used only byLoadLibraryExcalls that specifyLOAD_LIBRARY_SEARCH_USER_DIRS5- Clean up. The cookie returned by
AddDllDirectorycan be passed toRemoveDllDirectoryto remove the directory again5 - Print where it loaded from. Whether that one
GetModuleFileNameWline exists decides how long the investigation takes when something breaks
Note that the DLLs foo.dll depends on are also searched within this LOAD_LIBRARY_SEARCH_* scope.
If you want foo.dll’s own folder to be searched for its dependencies, make the first argument a full path and add LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR.
flowchart TB
accTitle: The flow of the minimal code example
accDescr: A diagram showing the flow of moving the process-default search space to the safe side with SetDefaultDllDirectories, explicitly adding the plugin folder with AddDllDirectory, loading with LoadLibraryEx and search flags, confirming where the module came from with GetModuleFileNameW, and cleaning up with RemoveDllDirectory.
p1["1. SetDefaultDllDirectories to make the default safe"] --> p2["2. AddDllDirectory to add the approved folder"]
p2 --> p3["3. LoadLibraryEx with search flags"]
p3 --> p4["4. GetModuleFileNameW to confirm the source"]
p4 --> p5["5. RemoveDllDirectory to clean up"]
Figure 17: The skeleton of the example is five steps: narrow, add, load, confirm, clean up.
8.6 Using It from C#
The same thinking applies in .NET. The P/Invoke search path is controlled with the DefaultDllImportSearchPaths attribute, and explicit loading with NativeLibrary.Load.
// .NET 8 / C# 12
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
// Restrict the default search location to System32 for every P/Invoke in this assembly.
// Limits: the attribute does not apply to a P/Invoke that specifies an absolute path,
// and it has no effect on platforms other than Windows.
[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
internal static class Program
{
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint GetTickCount();
private static void Main()
{
// Declaring the attribute alone does nothing. It only takes effect on a call.
Console.WriteLine($"GetTickCount = {GetTickCount()}");
// The plugin lives in its own folder. Do not write
// NativeLibrary.Load("foo.dll", asm, ApplicationDirectory | System32)
// here. ApplicationDirectory means the folder holding the exe and System32
// means the OS folder; neither one looks at the plugin folder.
// The load either fails or grabs some other foo.dll that happened to sit
// next to the exe. When you know where a file is, a full path is the sure way.
string pluginDir = Path.Combine(AppContext.BaseDirectory, "plugins");
string pluginPath = Path.GetFullPath(Path.Combine(pluginDir, "foo.dll"));
if (!File.Exists(pluginPath))
{
throw new FileNotFoundException("Plugin not found.", pluginPath);
}
// The overload that takes a path reads that file directly (it does not search).
IntPtr handle = NativeLibrary.Load(pluginPath);
try
{
// Confirm where it was loaded from.
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
{
if (string.Equals(module.ModuleName, "foo.dll", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"loaded from: {module.FileName}");
}
}
}
finally
{
NativeLibrary.Free(handle);
}
}
}
The values of DllImportSearchPath correspond to the LOAD_LIBRARY_SEARCH_* flags.
So when you write “System32 only” on the C# side, the constraints from 8.4 apply unchanged. The application directory is not searched.
The thing to watch here is that DllImportSearchPath has no value meaning “any folder I choose.” ApplicationDirectory means the folder holding the exe and System32 means the OS folder; there is no value that represents a folder you picked yourself for plugins. That is why search flags can never reach something placed in a dedicated folder, and why you end up specifying a full path as above.
Also note that even a full-path load does not pin the dependencies: as section 9 explains, the DLLs foo.dll depends on are still not fixed. If the plugin brings its own dependent DLLs, you need the same discipline as on the C side - add that folder with AddDllDirectory and enable LOAD_LIBRARY_SEARCH_USER_DIRS, or keep the dependencies together in one folder and use LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR.
flowchart TB
accTitle: How to load a DLL from a dedicated folder in C#
accDescr: A diagram showing that DllImportSearchPath has no value for an arbitrary folder because ApplicationDirectory means the folder holding the exe and System32 means the OS folder, so a plugin placed in a dedicated folder cannot be reached by search flags and must be loaded directly by full path, with dependent DLLs still needing the same discipline as on the C side.
q1["Put the plugin in a dedicated folder"] --> q2{"Can a search flag reach it"}
q2 -.->|"No value points at an arbitrary folder"| q3["Flags cannot reach it"]
q2 -->|"The sure way"| q4["Build the full path and load it directly with NativeLibrary.Load"]
q4 -.-> q5["Dependent DLLs still need the C-side discipline"]
Figure 18: In .NET too, a folder you chose yourself is best loaded by full path.
9. A Full Path Does Not Pin the Dependent DLLs
This is the point on this topic that matters a great deal in practice yet is easily overlooked. Microsoft Learn explains that even when the first DLL is loaded by full path, its dependent DLLs are searched by module name only.14
That is, just because
- you explicitly loaded
C:\\MyApp\\plugins\\foo.dll
it does not follow that
bar.dll, whichfoo.dlldepends on, will always be taken from the same folder.
With this misconception in place, you get the rather nasty failure mode of
- it works in the development environment,
- a different
bar.dllgets resolved at the deployment site, and - dependent-DLL conflicts become environment-dependent to reproduce.
flowchart TB
accTitle: A full path does not pin the dependent DLLs
accDescr: A diagram showing that even when the first DLL is loaded by full path its dependent DLLs are treated as searched by module name only, so they can be resolved from somewhere else, producing an environment-dependent failure where the code works in development but a different dependent DLL is resolved at the deployment site.
r1["Load foo.dll by full path"] --> r2["foo.dll itself loads exactly as specified"]
r1 --> r3["The dependent bar.dll is searched by module name only"]
r3 --> r4["It can be resolved from a different place per environment"]
r4 -.-> r5["Works in development, breaks at the deployment site"]
Figure 19: A full path pins only the first DLL; the dependencies are handled separately.
10. Avoiding DLL Preloading / Hijacking
Microsoft Learn’s DLL security documentation explains that the combination of dynamic loading without a full path and attacker-controllable directories in the search scope leads to DLL preloading attacks and binary planting attacks.2
In practice, it helps to converge on this baseline.
- Reduce bare-name loads like
LoadLibrary("foo.dll") - Use full-path loading where needed
- Narrow the process-default search path with
SetDefaultDllDirectories - Add only explicitly approved directories with
AddDllDirectory - Make the search scope explicit with flags like
LOAD_LIBRARY_SEARCH_SYSTEM32,LOAD_LIBRARY_SEARCH_APPLICATION_DIR, andLOAD_LIBRARY_SEARCH_USER_DIRS - Avoid the current folder and careless reliance on
PATH
A situation where a process running with administrator privileges has an ambiguous search path is especially dangerous. Microsoft Learn also notes that when a malicious DLL gets loaded, it executes with the privileges of that process.2
flowchart TB
accTitle: The combination that makes a DLL preloading attack work
accDescr: A diagram showing that dynamic loading without a full path combined with attacker-controllable directories in the search scope leads to a DLL preloading attack, that a malicious DLL then executes with the privileges of that process, and that the countermeasure is to reduce bare-name loads and narrow the search space.
s1["Dynamic loading without a full path"] --> s3["The conditions for DLL preloading are met"]
s2["Attacker-controllable directories in the search scope"] --> s3
s3 --> s4["A malicious DLL runs with the process's privileges"]
s3 -.-> s5["Fix: fewer bare names, narrower search space"]
Figure 20: The attack is the product of an ambiguous name and a controllable location.
11. Confirming Which DLL Was Loaded from Where
Everything so far has been about the specification. In an actual investigation, confirming instead of guessing is faster. Here are four options, by what you want to know.
| What you want to know | What to use | Where to look |
|---|---|---|
| Where it looked and where it found the file | Process Monitor10 | The record of file accesses |
| What is loaded right now | Process Explorer, tasklist /m11 |
The module list of the process |
| Which processes are holding a given DLL | ListDLLs12 | A list across all processes |
| The path actually loaded while debugging | The Modules window in Visual Studio | Debug > Windows > Modules7 |
11.1 Following the Search Trail with Process Monitor
This is the option with the most information in it. The procedure runs as follows.10
- Start Process Monitor as administrator and begin capturing
- Open Filter > Filter…, enter
Process Nameiswith the name of the target exe, and add it - On the same screen, add
Pathends with.dll - Start the target app, and stop capturing once the problem occurs
What you look at here is the Result column.
The loader tries to open locations from the top of the search order down, so the places that did not have the file line up as NAME NOT FOUND and the place that actually opened shows SUCCESS.
In other words, the line right after the run of NAME NOT FOUND entries is the path that was actually used.
Line this up against the table in section 3 and you can tell which stage decided the case. When a pre-stage rule (rows 1 through 6 of the table) decided it, there is no record of the loader going to the file system at all. That absence is an important clue in itself.
flowchart TB
accTitle: How to read a Process Monitor capture
accDescr: A diagram showing that because the loader tries to open locations from the top of the search order down, the places without the file line up as NAME NOT FOUND and the place that opened shows SUCCESS, so the line after the run of NAME NOT FOUND entries is the path actually used, and that no record at all means a pre-stage rule decided the outcome.
t1["Read the Result column from the top"] --> t2["A run of NAME NOT FOUND = places searched without a hit"]
t2 --> t3["The SUCCESS right after it is the path actually used"]
t1 -.-> t4["No record at all means a pre-stage rule decided it"]
Figure 21: The trail is in the Result column, and the absence of a record is itself a clue.
11.2 Listing the Loaded Modules
For a process that is already running, seeing what is loaded and from where takes no extra tooling.
tasklist /m foo.dll
This command lists the name and PID of every process that has the given module loaded.11 Once you have narrowed down which process you care about, switch the lower pane of Process Explorer to the DLL view and you can see the full path of every DLL that process has loaded.
Sysinternals ListDLLs does the same thing from the command line, and across all processes at once.12
This is the only way to tell whether the loaded-module list from 5.1 is in play. If a DLL of the same name was already loaded from another folder, that process will not go looking for a new one.
flowchart TB
accTitle: How to confirm the loaded modules
accDescr: A diagram showing that tasklist /m narrows down the processes holding the given module, that Process Explorer's DLL view or ListDLLs then shows the full path of what is loaded, and that this is the only way to see whether the loaded-module list is in play.
u1["tasklist /m foo.dll to find the processes holding it"] --> u2["Process Explorer or ListDLLs for the full path"]
u2 --> u3["Where it was loaded from becomes a settled fact"]
u3 -.-> u4["The effect of the loaded-module list is visible only here"]
Figure 22: What is loaded right now should be settled by a module list, not by guesswork.
12. A Practical Decision Checklist
When reviewing DLL loading design on Windows, checking at least the following reduces the number of things that go wrong.
- Is the app a packaged app or an unpackaged app?
- Which DLLs come from static linking, and which are dynamically loaded?
- Full-path loading, or module name only?
- Is
SetDllDirectoryin use anywhere? - Is the configuration such that
SetDefaultDllDirectoriesandLOAD_LIBRARY_SEARCH_*can be used? - Are multiple
AddDllDirectorycalls in use with an implicit expectation of ordering? - Are dependencies managed via manifests / SxS / private DLLs / redirection - and which one?
- Are there weak security assumptions around the current folder or
PATH? - Could dependent DLLs be resolved from a different location in a different environment?
- On a machine showing the symptom, has anyone confirmed where the DLL was actually loaded from (section 11)?
Checking these ten points separately lets you head off problems like “DLL not found,” “the wrong DLL was loaded,” “it only fails to start in production,” and “blocked at the vulnerability review” well before they surface.
flowchart TB
accTitle: The order to work through in a review
accDescr: A diagram showing that a DLL loading design review goes better when worked through in order: the shape of the app as packaged or unpackaged, how loading is done by full path or by name, how the APIs are used including SetDllDirectory and the search flags, how dependencies are managed, and finally confirming on a real machine where the DLL was loaded from.
v1["Check the shape of the app (packaged / unpackaged)"] --> v2["Check how it loads (full path or name only)"]
v2 --> v3["Check the API usage (SetDllDirectory, flags)"]
v3 --> v4["Check dependency management (manifest / SxS / PATH)"]
v4 --> v5["Confirm the load source on a real machine (section 11)"]
Figure 23: The checklist works through in order: shape, load, API, dependencies, real-machine check.
13. Summary
DLL name resolution on Windows is not merely a “folder search order.” In reality, it is determined by the overlap of DLL redirection, API sets, SxS manifests, the loaded-module list, Known DLLs, and the search space as modified by API calls.134
What matters most in practice comes down to these six things.
- Do not memorize the search order as a single table. Use the table to work out which stage decided your case
- Separate packaged from unpackaged
- Understand that even with full-path loading, dependent DLLs can be treated differently
- Do not use
SetDllDirectorycasually - To err on the safe side, use
SetDefaultDllDirectoriesand the search flags ofLoadLibraryEx - Do not stop at guesswork; confirm the path actually loaded with Process Monitor or something like it
DLL name resolution is a place where startup failures, environment differences, and security problems all tend to surface at once. That is exactly why it is worth understanding not just “in what order Windows searches,” but “what Windows treats as the premises of name resolution in the first place.”
flowchart TB
accTitle: The conclusion of this article
accDescr: A diagram showing that DLL name resolution is a place where startup failures, environment differences, and security problems all surface at once, so it is worth understanding not just the order in which Windows searches but what Windows treats as the premises of name resolution in the first place.
w1["In what order it searches (folder order)"] --> w3["Only both together explain the behavior"]
w2["What it treats as premises (pre-stage rules and search space)"] --> w3
w3 --> w4["Startup failures, environment differences, and security handled together"]
Figure 24: The practical knowledge is not the memorized order but the understanding of the premises.
Related articles
- A Minimum Security Checklist for Windows App Development
- Single-File Distribution of Windows Apps - Single Binaries and the Limits of OS Dependencies
- When Do You Actually Need Administrator Privileges on Windows? - UAC, Protected Areas, and How to Tell by Design
- What Is Reg-Free COM - Using COM Without Registration
References
- Microsoft Learn: Dynamic-link library search order
- Microsoft Learn: Dynamic-Link Library Security
- Microsoft Learn: Windows API sets
- Microsoft Learn: SetDefaultDllDirectories function
- Microsoft Learn: AddDllDirectory function
- Microsoft Learn: LoadLibraryEx function
- Microsoft Learn: Dynamic-link library redirection
- Microsoft Learn: Manifests
- Microsoft Learn: About Side-by-Side Assemblies
- Microsoft Learn: Process Monitor
- Microsoft Learn: ListDLLs
- Microsoft Learn: tasklist
-
Microsoft Learn, Dynamic-link library search order, accessed March 24, 2026 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25
-
Microsoft Learn, Dynamic-Link Library Security, accessed March 24, 2026 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Windows API sets, accessed March 24, 2026 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, SetDefaultDllDirectories function, accessed March 24, 2026 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, AddDllDirectory function, accessed March 24, 2026 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, LoadLibraryEx function, accessed March 24, 2026 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Dynamic-link library redirection, accessed March 24, 2026 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, About Side-by-Side Assemblies, accessed March 24, 2026 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Process Monitor ↩ ↩2
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Named Pipes in Practice — Windows' Standard IPC from Design to Security
A practical guide to named pipes, Windows' standard inter-process communication. This article organises, from primary sources, the choice...
DllMain and the Loader Lock — The Real Reason You're Told to "Do Nothing in DLL Initialization"
Why you must not call LoadLibrary or synchronize with other threads from DllMain. Drawing on primary sources, this article explains how t...
Why Windows Became What It Is Today: The Evolution of Windows Through a Developer's Eyes
A look at the changes from Windows 95 to Windows 11 — not as a visual timeline, but from a Windows application developer's perspective: c...
Why Windows Shows "Windows protected your PC"
Why SmartScreen warns when you distribute a Windows app, organized from a practitioner's perspective: code signing, EV/OV certificates, A...
When Do You Actually Need Administrator Privileges on Windows? - UAC, Protected Areas, and How to Tell by Design
A practical look at when administrator privileges are required on Windows, from the perspectives of UAC, protected areas, services, drive...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
DLL placement and loading directly affect whether a Windows app starts, how it is distributed, and how reproducible failures are, so this is well worth addressing in the context of Windows application development.
Technical Consulting & Design Review
DLL name resolution sits at the intersection of bug investigation, porting, vulnerability avoidance, and distribution design, making it an easy topic to work through as a design review or technical consultation.
Frequently Asked Questions
Common questions about the topic of this article.
- When I pass a DLL name to LoadLibrary, in what order does Windows look for it?
- A set of pre-stage rules is evaluated before the file system is walked at all. Specifically, DLL redirection, API sets, SxS manifest redirection, the loaded-module list, and Known DLLs take effect first, and only then does the search move on to the file system: the application folder, System32, the Windows folder, the current folder, and PATH. In the default state with safe DLL search mode enabled, the current folder sits quite far down. Note also that packaged apps and unpackaged apps use genuinely different search models.
- If I load a DLL by full path, will its dependent DLLs be loaded from the same folder?
- Not necessarily. Even when the first DLL is loaded by full path, its dependent DLLs are treated as searched by module name only, so they can be resolved from somewhere else. This misconception produces the awkward environment-dependent failure where everything works on the development machine but a different dependent DLL is resolved at the deployment site. To control the dependencies as well, make the search scope explicit with the LOAD_LIBRARY_SEARCH_* flags of LoadLibraryEx.
- Why should I avoid SetDllDirectory?
- Because it does more than change the search order: it behaves in a way that effectively disables safe DLL search mode. Even when all you meant to do was add one app-specific folder, the entire search space changes, including how the current folder is treated, and that can backfire from a security standpoint. On top of that, calling it in a parent process can affect the search order on the child process side. Leaning on SetDefaultDllDirectories, AddDllDirectory, and the LoadLibraryEx search flags instead is safer.
- How do I prevent DLL hijacking (DLL preloading attacks)?
- The attack comes from the combination of dynamic loading without a full path and attacker-controllable directories in the search scope, so narrow both. Concretely: reduce bare-name LoadLibrary calls, use full-path loading where needed, narrow the process-default search path with SetDefaultDllDirectories, add only approved directories with AddDllDirectory, and avoid the current folder and careless reliance on PATH. A process running with administrator privileges and an ambiguous search path is especially dangerous, because a malicious DLL then executes with that process's privileges.