-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This tries to ensure .runelite and its contents are writable by the user by changing the file acls whenever is it elevated. Co-authored-by: YvesW <[email protected]>
- Loading branch information
Showing
6 changed files
with
385 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
#include <Windows.h> | ||
#include <aclapi.h> | ||
#include <jni.h> | ||
#include <sddl.h> | ||
|
||
#include <memory> | ||
|
||
extern void rlThrow(JNIEnv *env, const char *msg); | ||
|
||
// https://learn.microsoft.com/en-us/windows/win32/secauthz/creating-a-security-descriptor-for-a-new-object-in-c--?redirectedfrom=MSDN | ||
extern "C" JNIEXPORT void JNICALL Java_net_runelite_launcher_Launcher_setFileACL(JNIEnv *env, jclass clazz, jstring folderJs, jobjectArray sidsJa) { | ||
SECURITY_DESCRIPTOR securityDescriptor; | ||
if (!InitializeSecurityDescriptor(&securityDescriptor, SECURITY_DESCRIPTOR_REVISION)) { | ||
rlThrow(env, "unable to initialize security descriptor"); | ||
return; | ||
} | ||
|
||
int numSid = env->GetArrayLength(sidsJa); | ||
EXPLICIT_ACCESSW *explicitAccesses = new EXPLICIT_ACCESSW[numSid]; | ||
ZeroMemory(explicitAccesses, sizeof(EXPLICIT_ACCESSW) * numSid); | ||
|
||
for (int i = 0; i < numSid; ++i) { | ||
jstring sidJs = static_cast<jstring>(env->GetObjectArrayElement(sidsJa, i)); | ||
const jchar *sid = env->GetStringChars(sidJs, nullptr); | ||
|
||
PSID pSid = NULL; | ||
if (!ConvertStringSidToSidW(reinterpret_cast<LPCWSTR>(sid), &pSid)) { | ||
rlThrow(env, "unable to convert string SID to SID"); | ||
env->ReleaseStringChars(sidJs, sid); | ||
goto freesid; | ||
} | ||
|
||
EXPLICIT_ACCESSW &explicitAccess = explicitAccesses[i]; | ||
explicitAccess.grfAccessPermissions = GENERIC_ALL; | ||
explicitAccess.grfAccessMode = SET_ACCESS; | ||
explicitAccess.grfInheritance = OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE; | ||
explicitAccess.Trustee.TrusteeForm = TRUSTEE_IS_SID; | ||
explicitAccess.Trustee.TrusteeType = TRUSTEE_IS_GROUP; | ||
explicitAccess.Trustee.ptstrName = (LPWSTR)pSid; | ||
|
||
env->ReleaseStringChars(sidJs, sid); | ||
} | ||
|
||
PACL pAcl = NULL; | ||
DWORD dwResult = SetEntriesInAclW(numSid, explicitAccesses, NULL, &pAcl); | ||
if (dwResult != ERROR_SUCCESS) { | ||
rlThrow(env, "unable to set entries in ACL"); | ||
goto freesid; | ||
} | ||
|
||
if (!SetSecurityDescriptorDacl(&securityDescriptor, TRUE, pAcl, FALSE)) { | ||
rlThrow(env, "error setting security descriptor DACL"); | ||
goto freeacl; | ||
} | ||
|
||
const jchar *folder = env->GetStringChars(folderJs, nullptr); | ||
if (!SetFileSecurityW(reinterpret_cast<LPCWSTR>(folder), DACL_SECURITY_INFORMATION, &securityDescriptor)) { | ||
rlThrow(env, "error setting file security"); | ||
} | ||
env->ReleaseStringChars(folderJs, folder); | ||
|
||
freeacl: | ||
LocalFree(pAcl); | ||
|
||
freesid: | ||
for (int i = 0; i < numSid; ++i) { | ||
EXPLICIT_ACCESSW &explicitAccess = explicitAccesses[i]; | ||
if (explicitAccess.Trustee.ptstrName) { | ||
LocalFree((PSID)explicitAccess.Trustee.ptstrName); | ||
} | ||
} | ||
|
||
delete[] explicitAccesses; | ||
} | ||
|
||
extern "C" JNIEXPORT jstring JNICALL Java_net_runelite_launcher_Launcher_getUserSID(JNIEnv *env, jclass clazz) { | ||
HANDLE hToken; | ||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) { | ||
rlThrow(env, "error opening process token"); | ||
return nullptr; | ||
} | ||
|
||
DWORD returnLength = 0; | ||
GetTokenInformation(hToken, TokenUser, nullptr, 0, &returnLength); | ||
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { | ||
rlThrow(env, "unable to get TokenUser buffer size"); | ||
CloseHandle(hToken); | ||
return nullptr; | ||
} | ||
|
||
std::unique_ptr<char[]> tokenInfoBuffer(new char[returnLength]); | ||
if (!GetTokenInformation(hToken, TokenUser, tokenInfoBuffer.get(), returnLength, &returnLength)) { | ||
rlThrow(env, "error getting token information"); | ||
CloseHandle(hToken); | ||
return nullptr; | ||
} | ||
|
||
CloseHandle(hToken); | ||
|
||
PTOKEN_USER tokenUser = (PTOKEN_USER)tokenInfoBuffer.get(); | ||
|
||
LPWSTR pstrSid; | ||
if (!ConvertSidToStringSidW(tokenUser->User.Sid, &pstrSid)) { | ||
rlThrow(env, "error converting SID to string"); | ||
return nullptr; | ||
} | ||
|
||
jstring ret = env->NewString(reinterpret_cast<const jchar *>(pstrSid), static_cast<jsize>(wcslen(pstrSid))); | ||
|
||
LocalFree(pstrSid); | ||
|
||
return ret; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
233 changes: 233 additions & 0 deletions
233
src/main/java/net/runelite/launcher/FilesystemPermissions.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
/* | ||
* Copyright (c) 2024, Adam <[email protected]> | ||
* All rights reserved. | ||
* | ||
* Redistribution and use in source and binary forms, with or without | ||
* modification, are permitted provided that the following conditions are met: | ||
* | ||
* 1. Redistributions of source code must retain the above copyright notice, this | ||
* list of conditions and the following disclaimer. | ||
* 2. Redistributions in binary form must reproduce the above copyright notice, | ||
* this list of conditions and the following disclaimer in the documentation | ||
* and/or other materials provided with the distribution. | ||
* | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
*/ | ||
package net.runelite.launcher; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import javax.swing.SwingUtilities; | ||
import lombok.extern.slf4j.Slf4j; | ||
import static net.runelite.launcher.Launcher.LAUNCHER_EXECUTABLE_NAME_WIN; | ||
import static net.runelite.launcher.Launcher.RUNELITE_DIR; | ||
import static net.runelite.launcher.Launcher.isProcessElevated; | ||
import static net.runelite.launcher.Launcher.nativesLoaded; | ||
import static net.runelite.launcher.Launcher.setFileACL; | ||
|
||
@Slf4j | ||
class FilesystemPermissions | ||
{ | ||
// https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids | ||
private static final String SID_SYSTEM = "S-1-5-18"; | ||
private static final String SID_ADMINISTRATORS = "S-1-5-32-544"; | ||
|
||
static boolean check() | ||
{ | ||
if (!nativesLoaded) | ||
{ | ||
log.debug("Launcher natives were not loaded. Skipping filesystem permission check."); | ||
return false; | ||
} | ||
|
||
final boolean elevated = isProcessElevated(ProcessHandle.current().pid()); | ||
// It is possible for .runelite to exist but be not writable, even when elevated. But we can update the ACLs | ||
// always when elevated, so attempt to fix the ACLs first. | ||
if (elevated) | ||
{ | ||
log.info("RuneLite is running as an administrator. This is not recommended because it can cause the files " + | ||
"RuneLite writes to {} to have more strict permissions than would otherwise be required.", | ||
RUNELITE_DIR); | ||
|
||
try | ||
{ | ||
final var sid = Launcher.getUserSID(); | ||
log.info("RuneLite is updating the ACLs of the files in {} to be: NT AUTHORITY\\SYSTEM, BUILTIN\\Administrators, " + | ||
"and {} (your user SID). To avoid this, don't run RuneLite with elevated permissions.", | ||
RUNELITE_DIR, sid); | ||
|
||
// Files.walk is depth-first, which doesn't work if the permissions on the root don't allow traversal. | ||
// So we do our own walk. | ||
setTreeACL(RUNELITE_DIR, sid); | ||
} | ||
catch (Exception ex) | ||
{ | ||
log.error("Unable to update file permissions", ex); | ||
} | ||
} | ||
|
||
if (!RUNELITE_DIR.exists()) | ||
{ | ||
if (!RUNELITE_DIR.mkdirs()) | ||
{ | ||
log.error("unable to create directory {} elevated: {}", RUNELITE_DIR, elevated); | ||
|
||
String message; | ||
if (elevated) | ||
{ | ||
message = "Unable to create RuneLite directory " + RUNELITE_DIR + " while elevated. Check your filesystem permissions are correct."; | ||
} | ||
else | ||
{ | ||
message = "Unable to create RuneLite directory " + RUNELITE_DIR + ". Check your filesystem permissions are correct. If you rerun RuneLite" + | ||
" as an administrator, RuneLite will attempt to create the directory again and fix its permissions."; | ||
} | ||
SwingUtilities.invokeLater(() -> | ||
{ | ||
var dialog = new FatalErrorDialog(message); | ||
if (!elevated) | ||
{ | ||
dialog.addButton("Run as administrator", FilesystemPermissions::runas); | ||
} | ||
dialog.open(); | ||
}); | ||
return true; | ||
} | ||
|
||
if (elevated) | ||
{ | ||
// Set the correct permissions on the newly created folder. This sets object inherit and container inherit, | ||
// so all future files in .runelite should then have the correct permissions. | ||
try | ||
{ | ||
final var sid = Launcher.getUserSID(); | ||
setTreeACL(RUNELITE_DIR, sid); | ||
} | ||
catch (Exception ex) | ||
{ | ||
log.error("Unable to update file permissions", ex); | ||
} | ||
} | ||
} | ||
|
||
if (!checkPermissions(RUNELITE_DIR)) | ||
{ | ||
String message; | ||
if (elevated) | ||
{ | ||
// This means the previous ACL update above did not work...? | ||
message = "The file permissions of " + RUNELITE_DIR + ", or a file within it, is not correct. Check the logs for more details."; | ||
} | ||
else | ||
{ | ||
message = "The file permissions of " + RUNELITE_DIR + ", or a file within it, is not correct. Check the logs for more details." + | ||
" If you rerun RuneLite as an administrator, RuneLite will attempt to fix the file permissions."; | ||
} | ||
SwingUtilities.invokeLater(() -> | ||
{ | ||
var dialog = new FatalErrorDialog(message); | ||
if (!elevated) | ||
{ | ||
dialog.addButton("Run as administrator", FilesystemPermissions::runas); | ||
} | ||
dialog.open(); | ||
}); | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private static boolean checkPermissions(File tree) | ||
{ | ||
boolean ok = true; | ||
for (File file : tree.listFiles()) | ||
{ | ||
if (file.isDirectory()) | ||
{ | ||
if (!checkPermissions(file)) | ||
{ | ||
ok = false; | ||
} | ||
} | ||
else | ||
{ | ||
Path path = file.toPath(); | ||
if (!Files.isReadable(path) || !Files.isWritable(path)) | ||
{ | ||
log.error("Permissions for {} are incorrect. Readable: {} writable: {}", | ||
file, Files.isReadable(path), Files.isWritable(path)); | ||
ok = false; | ||
} | ||
} | ||
} | ||
return ok; | ||
} | ||
|
||
private static void setTreeACL(File tree, String sid) throws IOException | ||
{ | ||
log.debug("Setting ACL on {}", tree.getAbsolutePath()); | ||
setFileACL(tree.getAbsolutePath(), new String[]{ | ||
SID_SYSTEM, | ||
SID_ADMINISTRATORS, | ||
sid | ||
}); | ||
Files.setAttribute(tree.toPath(), "dos:readonly", false); | ||
|
||
for (File file : tree.listFiles()) | ||
{ | ||
if (file.isDirectory()) | ||
{ | ||
setTreeACL(file, sid); | ||
} | ||
else | ||
{ | ||
log.debug("Setting ACL on {}", file.getAbsolutePath()); | ||
setFileACL(file.getAbsolutePath(), new String[]{ | ||
SID_SYSTEM, | ||
SID_ADMINISTRATORS, | ||
sid | ||
}); | ||
Files.setAttribute(file.toPath(), "dos:readonly", false); | ||
} | ||
} | ||
} | ||
|
||
private static void runas() | ||
{ | ||
log.info("Relaunching as administrator"); | ||
|
||
ProcessHandle current = ProcessHandle.current(); | ||
var command = current.info().command(); | ||
if (command.isEmpty()) | ||
{ | ||
log.error("Running process has no command"); | ||
System.exit(-1); | ||
return; | ||
} | ||
|
||
Path path = Paths.get(command.get()); | ||
if (!path.getFileName().toString().equals(LAUNCHER_EXECUTABLE_NAME_WIN)) | ||
{ | ||
log.error("Running process is not the launcher: {}", path.getFileName().toString()); | ||
System.exit(-1); | ||
return; | ||
} | ||
|
||
String commandPath = path.toAbsolutePath().toString(); | ||
Launcher.runas(commandPath, ""); | ||
System.exit(0); | ||
} | ||
} |
Oops, something went wrong.