From bc07627ecf743de52900fb82e6779eb625a37627 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Fri, 24 Jul 2026 13:47:53 +0000
Subject: [PATCH] Fix all cpp/ CodeQL code-scanning alerts in Windows build-tools (#757)
---
opendj-server-legacy/src/build-tools/windows/winlauncher.c | 18 ++-
opendj-server-legacy/src/build-tools/windows/common.h | 10 +
opendj-server-legacy/src/build-tools/windows/service.c | 215 +++++++++++++++++++----------------
opendj-server-legacy/src/build-tools/windows/service.h | 5
opendj-server-legacy/src/build-tools/windows/common.c | 67 +++++++---
5 files changed, 187 insertions(+), 128 deletions(-)
diff --git a/opendj-server-legacy/src/build-tools/windows/common.c b/opendj-server-legacy/src/build-tools/windows/common.c
index 2221221..bd62f08 100644
--- a/opendj-server-legacy/src/build-tools/windows/common.c
+++ b/opendj-server-legacy/src/build-tools/windows/common.c
@@ -13,10 +13,12 @@
*
* Copyright 2008-2010 Sun Microsystems, Inc.
* Portions Copyright 2013 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
#include "common.h"
#include "service.h"
+#include <direct.h>
#include <errno.h>
#include <fcntl.h>
#include <io.h>
@@ -112,7 +114,7 @@
debug("createBatchFileChildProcess: the batch file path is too long.");
return FALSE;
}
- sprintf(command, "/c %s", batchFile);
+ _snprintf(command, COMMAND_SIZE, "/c %s", batchFile);
debug("createBatchFileChildProcess: Attempting to create child process '%s' background=%d.",
command,
background);
@@ -259,7 +261,7 @@
char * logFile;
FILE *fp;
time_t rawtime;
- struct tm * timeinfo;
+ struct tm timeinfo;
char formattedTime[100];
if (noMessageLogged)
@@ -272,8 +274,8 @@
// Time-stamp
time(&rawtime);
- timeinfo = localtime(&rawtime);
- strftime(formattedTime, 100, "%Y/%m/%d %H:%M:%S", timeinfo);
+ localtime_s(&timeinfo, &rawtime);
+ strftime(formattedTime, 100, "%Y/%m/%d %H:%M:%S", &timeinfo);
logFile = getDebugLogFileName();
deleteIfLargerThan(logFile, MAX_DEBUG_LOG_SIZE);
@@ -310,16 +312,13 @@
char execName [MAX_PATH];
char * lastSlash;
char logpath[MAX_PATH];
- char * temp;
- FILE *file;
- if (logFile != NULL)
+ if (logFile != NULL)
{
return logFile;
}
- temp = getenv("TEMP");
-
- // Get the name of the executable.
+
+ // Get the name of the executable.
GetModuleFileName (
NULL,
execName,
@@ -339,18 +338,36 @@
strcpy(logpath, execName);
strcat(logpath, "\\logs\\");
- // If the log folder doesn's exist in the instance path
+ // If the log folder doesn't exist in the instance path
// we create the log file in the temp directory.
- if (isExistingDirectory(logpath))
+ if (isExistingDirectory(logpath))
{
- sprintf(path, "%s\\logs\\%s", execName, DEBUG_LOG_NAME);
- } else {
- strcat(temp, "\\logs\\");
- mkdir(temp);
- strcat(temp, DEBUG_LOG_NAME);
- file = fopen(temp,"a+");
- fclose(file);
- sprintf(path, "%s", temp);
+ _snprintf(path, MAX_PATH, "%s\\logs\\%s", execName, DEBUG_LOG_NAME);
+ path[MAX_PATH - 1] = '\0';
+ }
+ else
+ {
+ // Fall back to the OS temp directory. Use GetTempPath (a trusted OS API
+ // that resolves the TMP/TEMP/USERPROFILE locations) rather than
+ // getenv("TEMP"), whose buffer must not be extended in place.
+ char tempDir[MAX_PATH];
+ DWORD tempLen = GetTempPath(MAX_PATH, tempDir);
+ if ((tempLen > 0) && (tempLen < MAX_PATH))
+ {
+ // GetTempPath returns a path that already ends with a backslash.
+ char tempLogDir[MAX_PATH];
+ _snprintf(tempLogDir, MAX_PATH, "%slogs\\", tempDir);
+ tempLogDir[MAX_PATH - 1] = '\0';
+ _mkdir(tempLogDir);
+ _snprintf(path, MAX_PATH, "%s%s", tempLogDir, DEBUG_LOG_NAME);
+ path[MAX_PATH - 1] = '\0';
+ }
+ else
+ {
+ // Could not determine a temp directory: fall back to the log file name.
+ _snprintf(path, MAX_PATH, "%s", DEBUG_LOG_NAME);
+ path[MAX_PATH - 1] = '\0';
+ }
}
logFile = _strdup(path);
@@ -415,6 +432,14 @@
{
DWORD str = GetFileAttributes(fileName);
- return (str != INVALID_FILE_ATTRIBUTES &&
+ return (str != INVALID_FILE_ATTRIBUTES &&
(str & FILE_ATTRIBUTE_DIRECTORY));
}
+
+// ---------------------------------------------------------------
+// See common.h for the contract of this function.
+// ---------------------------------------------------------------
+BOOL isSafePath(const char* path)
+{
+ return (path != NULL) && (strstr(path, "..") == NULL);
+}
diff --git a/opendj-server-legacy/src/build-tools/windows/common.h b/opendj-server-legacy/src/build-tools/windows/common.h
index d666576..405e405 100644
--- a/opendj-server-legacy/src/build-tools/windows/common.h
+++ b/opendj-server-legacy/src/build-tools/windows/common.h
@@ -12,8 +12,12 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2008-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
+#ifndef OPENDJ_WINDOWS_COMMON_H
+#define OPENDJ_WINDOWS_COMMON_H
+
// Just some functions and constants to be used by winlauncher.c
// and service.c
@@ -32,3 +36,9 @@
void updateDebugFlag(char* argv[], int argc);
BOOL waitForProcess(PROCESS_INFORMATION* procInfo, DWORD waitTime,
DWORD* exitCode);
+
+// Returns TRUE if the given path is non-NULL and does not contain a
+// parent-directory reference ("..") that could be used for path traversal.
+BOOL isSafePath(const char* path);
+
+#endif // OPENDJ_WINDOWS_COMMON_H
diff --git a/opendj-server-legacy/src/build-tools/windows/service.c b/opendj-server-legacy/src/build-tools/windows/service.c
index 1fd7e37..587cc32 100644
--- a/opendj-server-legacy/src/build-tools/windows/service.c
+++ b/opendj-server-legacy/src/build-tools/windows/service.c
@@ -190,7 +190,7 @@
// Check whether the Registry Key is already created,
// If so don't create a new one.
- sprintf (subkey, EVENT_LOG_KEY, serviceName);
+ _snprintf (subkey, MAX_REGISTRY_KEY, EVENT_LOG_KEY, serviceName);
result = RegOpenKeyEx(
HKEY_LOCAL_MACHINE,
subkey,
@@ -292,7 +292,7 @@
// Set the number of categories: 1 (OPENDJ)
if (success)
{
- long result = RegSetValueEx(
+ result = RegSetValueEx(
hkey, // subkey handle
"CategoryCount", // value name
0, // must be zero
@@ -346,7 +346,7 @@
// Check whether the Registry Key is already created,
// If so don't create a new one.
- sprintf (subkey, EVENT_LOG_KEY, serviceName);
+ _snprintf (subkey, MAX_REGISTRY_KEY, EVENT_LOG_KEY, serviceName);
result = RegOpenKeyEx(
HKEY_LOCAL_MACHINE,
subkey,
@@ -390,7 +390,10 @@
char subkey [MAX_SERVICE_NAME];
debug("Registering the Event Log for service '%s'.", serviceName);
- sprintf (subkey, serviceName);
+ // _snprintf does not null-terminate on truncation (MSVC), so terminate
+ // explicitly: serviceName ultimately derives from argv and is unbounded.
+ _snprintf (subkey, MAX_SERVICE_NAME, "%s", serviceName);
+ subkey[MAX_SERVICE_NAME - 1] = '\0';
eventLog = RegisterEventSource(
NULL, // local host
@@ -427,11 +430,11 @@
{
debug("Determining if the server is running.");
}
- if (strlen(relativePath)+strlen(_instanceDir)+1 < MAX_PATH)
+ if (isSafePath(_instanceDir) && strlen(relativePath)+strlen(_instanceDir)+1 < MAX_PATH)
{
int fd;
- sprintf(lockFile, "%s%s", _instanceDir, relativePath);
+ _snprintf(lockFile, MAX_PATH, "%s%s", _instanceDir, relativePath);
if (mustDebug)
{
debug(
@@ -517,7 +520,7 @@
if (strlen(relativePath)+strlen(_instanceDir)+1 < COMMAND_SIZE)
{
- sprintf(command, "\"%s%s\" --windowsNetStart", _instanceDir, relativePath);
+ _snprintf(command, COMMAND_SIZE, "\"%s%s\" --windowsNetStart", _instanceDir, relativePath);
debug("doStartApplication: Launching batch file: %s", command);
createOk = createBatchFileChildProcess(command, FALSE, &procInfo);
@@ -709,7 +712,7 @@
debug("doStopApplication");
if (strlen(relativePath)+strlen(_instanceDir)+1 < COMMAND_SIZE)
{
- sprintf(command, "\"%s%s\" --windowsNetStop", _instanceDir, relativePath);
+ _snprintf(command, COMMAND_SIZE, "\"%s%s\" --windowsNetStop", _instanceDir, relativePath);
// launch the command
if (spawn(command, FALSE) != -1)
@@ -808,7 +811,7 @@
if ((strlen(fileName) + strlen(" start ") + strlen(_instanceDir) +
2 * strlen("\"\"")) < COMMAND_SIZE)
{
- sprintf(serviceBinPath, "\"%s\" start \"%s\"", fileName,
+ _snprintf(serviceBinPath, COMMAND_SIZE, "\"%s\" start \"%s\"", fileName,
_instanceDir);
}
else
@@ -873,7 +876,7 @@
// This function assumes that there are at least
// MAX_SERVICE_NAME (256) characters reserved in
// servicename.
- sprintf(serviceName, curService.serviceName);
+ _snprintf(serviceName, MAX_SERVICE_NAME, "%s", curService.serviceName);
returnValue = SERVICE_RETURN_OK;
}
else
@@ -1061,11 +1064,11 @@
ServiceReturnCode code;
// a checkpoint value indicate the progress of an operation
DWORD checkPoint = CHECKPOINT_FIRST_VALUE;
- SERVICE_STATUS_HANDLE serviceStatusHandle;
+ // static so its address does not escape the stack when stored in the
+ // global _serviceStatusHandle; there is only one service per process.
+ static SERVICE_STATUS_HANDLE serviceStatusHandle;
BOOL running;
- // __debugbreak();
-
debug("serviceMain");
code = createServiceBinPath(cmdToRun);
@@ -1133,16 +1136,12 @@
if (!running && code == SERVICE_RETURN_OK)
{
WORD argCount = 1;
- const char *argc[] = {_instanceDir};
+ const char *logArgs[] = {_instanceDir};
code = doStartApplication(_serviceStatusHandle, &checkPoint);
- switch (code)
+ if (code != SERVICE_RETURN_OK)
{
- case SERVICE_RETURN_OK:
- // start is ok: do nothing for the moment.
- break;
-
- default:
+ // start failed
debugError("serviceMain: doStartApplication() failed");
code = SERVICE_RETURN_ERROR;
_serviceCurStatus = SERVICE_STOPPED;
@@ -1156,7 +1155,7 @@
reportLogEvent(
EVENTLOG_ERROR_TYPE,
WIN_EVENT_ID_SERVER_START_FAILED,
- argCount, argc
+ argCount, logArgs
);
}
}
@@ -1222,7 +1221,7 @@
if (!updatedRunningStatus)
{
WORD argCount = 1;
- const char *argc[] = {_instanceDir};
+ const char *logArgs[] = {_instanceDir};
updateServiceStatus (
_serviceCurStatus,
NO_ERROR,
@@ -1234,7 +1233,7 @@
reportLogEvent(
EVENTLOG_SUCCESS,
WIN_EVENT_ID_SERVER_STARTED,
- argCount, argc
+ argCount, logArgs
);
updatedRunningStatus = TRUE;
}
@@ -1269,7 +1268,7 @@
(state == SERVICE_STOP_PENDING))))
{
WORD argCount = 1;
- const char *argc[] = {_instanceDir};
+ const char *logArgs[] = {_instanceDir};
_serviceCurStatus = SERVICE_STOPPED;
debug("checking in serviceMain serviceHandler: service stopped with error.");
@@ -1283,7 +1282,7 @@
reportLogEvent(
EVENTLOG_ERROR_TYPE,
WIN_EVENT_ID_SERVER_STOPPED_OUTSIDE_SCM,
- argCount, argc);
+ argCount, logArgs);
}
break;
}
@@ -1324,6 +1323,68 @@
} // doTerminateService
// ----------------------------------------------------
+// Stops the service from within the service handler. Shared by the
+// SERVICE_CONTROL_STOP and SERVICE_CONTROL_SHUTDOWN control codes.
+// ----------------------------------------------------
+static void stopServiceFromHandler(void)
+{
+ ServiceReturnCode code;
+ DWORD checkpoint;
+
+ // update service status to STOP_PENDING
+ debug("serviceHandler: stop");
+ _serviceCurStatus = SERVICE_STOP_PENDING;
+ checkpoint = CHECKPOINT_FIRST_VALUE;
+ updateServiceStatus (
+ _serviceCurStatus,
+ NO_ERROR,
+ 0,
+ checkpoint++,
+ TIMEOUT_STOP_SERVICE,
+ _serviceStatusHandle
+ );
+
+ // let's try to stop the application whatever may be the status above
+ // (best effort mode)
+ code = doStopApplication();
+ if (code == SERVICE_RETURN_OK)
+ {
+ WORD argCount = 1;
+ const char *logArgs[] = {_instanceDir};
+ _serviceCurStatus = SERVICE_STOPPED;
+ updateServiceStatus (
+ _serviceCurStatus,
+ NO_ERROR,
+ 0,
+ CHECKPOINT_NO_ONGOING_OPERATION,
+ TIMEOUT_NONE,
+ _serviceStatusHandle
+ );
+
+ // again, let's ignore the above status and
+ // notify serviceMain that service has stopped
+ doTerminateService (_terminationEvent);
+ reportLogEvent(
+ EVENTLOG_SUCCESS,
+ WIN_EVENT_ID_SERVER_STOP,
+ argCount, logArgs
+ );
+ }
+ else
+ {
+ WORD argCount = 1;
+ const char *logArgs[] = {_instanceDir};
+ debug("serviceHandler: The server could not be stopped.");
+ // We could not stop the server
+ reportLogEvent(
+ EVENTLOG_ERROR_TYPE,
+ WIN_EVENT_ID_SERVER_STOP_FAILED,
+ argCount, logArgs
+ );
+ }
+} // stopServiceFromHandler
+
+// ----------------------------------------------------
// This function is the handler of the service. It is processing the
// commands send by the SCM. Commands can be: STOP, PAUSE, CONTINUE,
// SHUTDOWN and INTERROGATE.
@@ -1333,7 +1394,6 @@
void serviceHandler(DWORD controlCode)
{
ServiceReturnCode code;
- DWORD checkpoint;
BOOL running;
debug("serviceHandler called with controlCode=%d.", controlCode);
switch (controlCode)
@@ -1343,60 +1403,9 @@
// -> no break here
debug("serviceHandler: shutdown");
case SERVICE_CONTROL_STOP:
- {
- // update service status to STOP_PENDING
- debug("serviceHandler: stop");
- _serviceCurStatus = SERVICE_STOP_PENDING;
- checkpoint = CHECKPOINT_FIRST_VALUE;
- updateServiceStatus (
- _serviceCurStatus,
- NO_ERROR,
- 0,
- checkpoint++,
- TIMEOUT_STOP_SERVICE,
- _serviceStatusHandle
- );
-
- // let's try to stop the application whatever may be the status above
- // (best effort mode)
- code = doStopApplication();
- if (code == SERVICE_RETURN_OK)
- {
- WORD argCount = 1;
- const char *argc[] = {_instanceDir};
- _serviceCurStatus = SERVICE_STOPPED;
- updateServiceStatus (
- _serviceCurStatus,
- NO_ERROR,
- 0,
- CHECKPOINT_NO_ONGOING_OPERATION,
- TIMEOUT_NONE,
- _serviceStatusHandle
- );
-
- // again, let's ignore the above status and
- // notify serviceMain that service has stopped
- doTerminateService (_terminationEvent);
- reportLogEvent(
- EVENTLOG_SUCCESS,
- WIN_EVENT_ID_SERVER_STOP,
- argCount, argc
- );
- }
- else
- {
- WORD argCount = 1;
- const char *argc[] = {_instanceDir};
- debug("serviceHandler: The server could not be stopped.");
- // We could not stop the server
- reportLogEvent(
- EVENTLOG_ERROR_TYPE,
- WIN_EVENT_ID_SERVER_STOP_FAILED,
- argCount, argc
- );
- }
+ // update service status to STOP_PENDING and stop the application
+ stopServiceFromHandler();
break;
- }
// Request to pause the service
@@ -1526,7 +1535,7 @@
{
if (strlen(serviceConfig->lpBinaryPathName) < COMMAND_SIZE)
{
- sprintf(binPathName, serviceConfig->lpBinaryPathName);
+ _snprintf(binPathName, COMMAND_SIZE, "%s", serviceConfig->lpBinaryPathName);
returnValue = SERVICE_RETURN_OK;
}
else
@@ -1543,11 +1552,8 @@
}
}
- // free buffers
- if (serviceConfig != NULL)
- {
- free(serviceConfig);
- }
+ // free buffers (free(NULL) is a safe no-op, so no guard is needed)
+ free(serviceConfig);
return returnValue;
} // getBinaryPathName
@@ -1618,12 +1624,12 @@
if (! svcStatusOk)
{
- DWORD lastError = GetLastError();
- if (lastError != ERROR_MORE_DATA)
+ DWORD innerLastError = GetLastError();
+ if (innerLastError != ERROR_MORE_DATA)
{
returnValue = SERVICE_RETURN_ERROR;
debug("getServiceList: second try generic error. Code [%d]",
- lastError);
+ innerLastError);
}
else
{
@@ -1690,8 +1696,10 @@
if (scm != NULL)
{
CloseServiceHandle (scm);
- // free the result buffer
- if (lpServiceData != NULL)
+ // free the result buffer, but only if it was heap-allocated: it still
+ // points to the stack-based serviceData when the first EnumServicesStatus
+ // call succeeded without needing a larger buffer.
+ if (lpServiceData != &serviceData)
{
free (lpServiceData);
}
@@ -1780,12 +1788,15 @@
{
if (i == 1)
{
- sprintf(serviceName, baseName);
+ _snprintf(serviceName, MAX_SERVICE_NAME, "%s", baseName);
}
else
{
- sprintf(serviceName, "%s-%d", baseName, i);
+ _snprintf(serviceName, MAX_SERVICE_NAME, "%s-%d", baseName, i);
}
+ // _snprintf does not null-terminate on truncation (MSVC), and baseName
+ // (== argv[3]) is unbounded, so terminate explicitly.
+ serviceName[MAX_SERVICE_NAME - 1] = '\0';
nameInUseResult = serviceNameInUse(serviceName);
@@ -1930,11 +1941,8 @@
CloseServiceHandle (scm);
}
- // free names
- if (serviceName != NULL)
- {
- free (serviceName);
- }
+ // free names (free(NULL) is a safe no-op, so no guard is needed)
+ free (serviceName);
debug("createServiceInScm returning %d.", returnValue);
@@ -1970,7 +1978,7 @@
serviceName,
SERVICE_ALL_ACCESS | DELETE
);
- debug("After opening service myService=%d.", myService);
+ debug("After opening service myService=%p.", (void*)myService);
if (myService == NULL)
{
debugError("Failed to open the service '%s'. Last error = %d",
@@ -2176,7 +2184,7 @@
{
// There is a valid serviceName for the command to run, so
// OpenDJ is registered as a service.
- fprintf(stdout, serviceName);
+ fprintf(stdout, "%s", serviceName);
returnCode = 0;
debug("Service '%s' is enabled.", serviceName);
}
@@ -2375,6 +2383,13 @@
+// ---------------------------------------------------------------
+// Entry point for the opendj_service executable. Dispatches to the
+// requested subcommand (create, state, remove, start, isrunning or
+// cleanup); each operates on the OpenDJ instance directory (or the
+// service name, for cleanup) given as the following argument.
+// Returns 0 on success and a non-zero value otherwise.
+// ---------------------------------------------------------------
int main(int argc, char* argv[])
{
char* subcommand;
@@ -2388,8 +2403,6 @@
debug(" argv[%d] = '%s'", i, argv[i]);
}
- // __debugbreak();
-
if (argc <= 1)
{
fprintf(stdout,
diff --git a/opendj-server-legacy/src/build-tools/windows/service.h b/opendj-server-legacy/src/build-tools/windows/service.h
index d75deba..96ad255 100644
--- a/opendj-server-legacy/src/build-tools/windows/service.h
+++ b/opendj-server-legacy/src/build-tools/windows/service.h
@@ -16,6 +16,9 @@
* Portions Copyright 2026 3A Systems, LLC.
*/
+#ifndef OPENDJ_WINDOWS_SERVICE_H
+#define OPENDJ_WINDOWS_SERVICE_H
+
#include "common.h"
#include <errno.h>
#include <fcntl.h>
@@ -100,3 +103,5 @@
void serviceHandler(DWORD controlCode);
BOOL getServiceStatus(char *serviceName, LPDWORD returnState);
+#endif // OPENDJ_WINDOWS_SERVICE_H
+
diff --git a/opendj-server-legacy/src/build-tools/windows/winlauncher.c b/opendj-server-legacy/src/build-tools/windows/winlauncher.c
index 2e5d851..e449a13 100644
--- a/opendj-server-legacy/src/build-tools/windows/winlauncher.c
+++ b/opendj-server-legacy/src/build-tools/windows/winlauncher.c
@@ -12,6 +12,7 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2008 Sun Microsystems, Inc.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
#include "winlauncher.h"
@@ -29,9 +30,14 @@
debug("Attempting to get the PID file for instanceDir='%s'", instanceDir);
- if ((strlen(relativePath) + strlen(instanceDir)) < maxSize)
+ if (!isSafePath(instanceDir))
{
- sprintf(pidFile, "%s\\logs\\server.pid", instanceDir);
+ debugError("Unable to get the PID file name because the instance dir is not a safe path.");
+ returnValue = FALSE;
+ }
+ else if ((strlen(relativePath) + strlen(instanceDir)) < maxSize)
+ {
+ _snprintf(pidFile, maxSize, "%s\\logs\\server.pid", instanceDir);
returnValue = TRUE;
debug("PID file name is '%s'.", pidFile);
}
@@ -283,7 +289,7 @@
{
if (curCmdInd + strlen(" ") < maxSize)
{
- sprintf (&command[curCmdInd], " ");
+ _snprintf (&command[curCmdInd], maxSize - curCmdInd, " ");
curCmdInd = strlen(command);
}
else
@@ -314,7 +320,7 @@
if (curCmdInd + strlen("\"\"") + strlen(curarg) < maxSize)
{
// no begining quote and white space inside => add quotes
- sprintf (&command[curCmdInd], "\"%s\"", curarg);
+ _snprintf (&command[curCmdInd], maxSize - curCmdInd, "\"%s\"", curarg);
curCmdInd = strlen (command);
}
else
@@ -327,7 +333,7 @@
if (curCmdInd + strlen(curarg) < maxSize)
{
// no white space or quotes detected, keep the arg as is
- sprintf (&command[curCmdInd], "%s", curarg);
+ _snprintf (&command[curCmdInd], maxSize - curCmdInd, "%s", curarg);
curCmdInd = strlen (command);
}
else
@@ -339,7 +345,7 @@
} else {
if (curCmdInd + strlen("\"\"") < maxSize)
{
- sprintf (&command[curCmdInd], "\"\"");
+ _snprintf (&command[curCmdInd], maxSize - curCmdInd, "\"\"");
curCmdInd = strlen (command);
}
else
--
Gitblit v1.10.0