Skip to content

Commit e499927

Browse files
authored
Fix phpGH-17658: COMPersistHelper::LoadFromStream() can segfault (phpGH-17659)
* Fix phpGH-17658: COMPersistHelper::LoadFromStream() can segfault The actual fix is trivial, but to be able to test the behavior we have to introduce an own COM object, since existing persistable objects likely implement `IPersistInit`, not only `IPersist`. We also want to avoid further test dependencies on possibly unavailable objects, such as `Word.Application`. To this purposes, we add a small COM in-process server, which may be extended for other testing purposes. We keep it simple by implementing it in C++, but without using any more sophisticated frameworks like ATL. This component needs to be built explicitly (`nmake comtest.dll`), and also needs to be explicitly registered (`nmake register_comtest`). When no longer needed, it is possible to unregister the component (`nmake unregister_comtest`).
1 parent e6c570a commit e499927

File tree

9 files changed

+417
-1
lines changed

9 files changed

+417
-1
lines changed

.github/scripts/windows/build_task.bat

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ cmd /c configure.bat ^
4545
if %errorlevel% neq 0 exit /b 3
4646

4747
nmake /NOLOGO
48+
nmake /NOLOGO comtest.dll
4849
if %errorlevel% neq 0 exit /b 3
4950

5051
exit /b 0

.github/scripts/windows/test_task.bat

+4
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ hMailServer.exe /verysilent
118118
cd %APPVEYOR_BUILD_FOLDER%
119119
%PHP_BUILD_DIR%\php.exe -dextension_dir=%PHP_BUILD_DIR% -dextension=com_dotnet .github\setup_hmailserver.php
120120

121+
rem prepare for com_dotnet
122+
nmake register_comtest
123+
121124
mkdir %PHP_BUILD_DIR%\test_file_cache
122125
rem generate php.ini
123126
echo extension_dir=%PHP_BUILD_DIR% > %PHP_BUILD_DIR%\php.ini
@@ -146,6 +149,7 @@ nmake test TESTS="%OPCACHE_OPTS% -g FAIL,BORK,LEAK,XLEAK %ASAN_OPTS% --no-progre
146149

147150
set EXIT_CODE=%errorlevel%
148151

152+
nmake unregister_comtest
149153
taskkill /f /im snmpd.exe
150154

151155
exit /b %EXIT_CODE%

ext/com_dotnet/Makefile.frag.w32

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.c: ext\com_dotnet\tests\comtest\comtest.idl
2+
-md $(BUILD_DIR)\ext\com_dotnet\tests\comtest
3+
midl /nologo /h $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.h /iid $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.c /tlb $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.tlb ext\com_dotnet\tests\comtest\comtest.idl
4+
5+
$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.obj $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.obj: ext\com_dotnet\tests\comtest\comtest.cpp $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.c
6+
$(PHP_CL) /nologo /c /Fo$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.obj /I $(BUILD_DIR)\ext\com_dotnet\tests\comtest ext\com_dotnet\tests\comtest\comtest.cpp
7+
$(PHP_CL) /nologo /c /Fo$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.obj $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.c
8+
9+
$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.dll: $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.obj $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.obj ext\com_dotnet\tests\comtest\comtest.def
10+
"$(LINK)" /nologo /dll /out:$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.dll $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.obj $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.obj /def:ext\com_dotnet\tests\comtest\comtest.def OleAut32.lib
11+
12+
comtest.dll: $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.dll
13+
14+
register_comtest:
15+
@reg add HKCU\SOFTWARE\Classes\TypeLib\{AE8685BE-3758-4BDA-91DB-1459EBA24747} /f > NUL
16+
@reg add HKCU\SOFTWARE\Classes\TypeLib\{AE8685BE-3758-4BDA-91DB-1459EBA24747}\1.0 /d "PHP COM Test Library" /f > NUL
17+
@reg add HKCU\SOFTWARE\Classes\TypeLib\{AE8685BE-3758-4BDA-91DB-1459EBA24747}\1.0\0 /f > NUL
18+
@reg add HKCU\SOFTWARE\Classes\TypeLib\{AE8685BE-3758-4BDA-91DB-1459EBA24747}\1.0\0\win32 /d "$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.tlb" /f > NUL
19+
@reg add HKCU\SOFTWARE\Classes\TypeLib\{AE8685BE-3758-4BDA-91DB-1459EBA24747}\1.0\0\win64 /d "$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.tlb" /f > NUL
20+
@reg add HKCU\SOFTWARE\Classes\TypeLib\{AE8685BE-3758-4BDA-91DB-1459EBA24747}\1.0\FLAGS /d 0 /f > NUL
21+
@reg add HKCU\SOFTWARE\Classes\TypeLib\{AE8685BE-3758-4BDA-91DB-1459EBA24747}\1.0\HELPDIR /d "$(BUILD_DIR)\ext\com_dotnet\tests\comtest" /f > NUL
22+
@reg add HKCU\SOFTWARE\Classes\CLSID\{B13FE324-D595-44C7-97D7-82CE20EDF878} /d "PHP COM Test Document" /f > NUL
23+
@reg add HKCU\SOFTWARE\Classes\CLSID\{B13FE324-D595-44C7-97D7-82CE20EDF878}\InprocServer32 /d "$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.dll" /f > NUL
24+
@reg add HKCU\SOFTWARE\Classes\PHPTest.Document /d "PHP COM Test Document" /f > NUL
25+
@reg add HKCU\SOFTWARE\Classes\PHPTest.Document\CLSID /d "{B13FE324-D595-44C7-97D7-82CE20EDF878}" /f > NUL
26+
27+
unregister_comtest:
28+
-@reg delete HKCU\SOFTWARE\Classes\PHPTest.Document /f > NUL
29+
-@reg delete HKCU\SOFTWARE\Classes\CLSID\{B13FE324-D595-44C7-97D7-82CE20EDF878} /f > NUL
30+
-@reg delete HKCU\SOFTWARE\Classes\TypeLib\{AE8685BE-3758-4BDA-91DB-1459EBA24747} /f > NUL

ext/com_dotnet/com_persist.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ CPH_METHOD(LoadFromStream)
557557
} else {
558558
res = get_persist_stream(helper);
559559
if (helper->ips) {
560-
res = IPersistStreamInit_Load(helper->ipsi, stm);
560+
res = IPersistStream_Load(helper->ips, stm);
561561
}
562562
}
563563
}

ext/com_dotnet/config.w32

+1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ if (PHP_COM_DOTNET == "yes") {
1010
null, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1");
1111
AC_DEFINE('HAVE_COM_DOTNET', 1, "Define to 1 if the PHP extension 'com_dotnet' is available.");
1212
CHECK_HEADER_ADD_INCLUDE('mscoree.h', 'CFLAGS_COM_DOTNET');
13+
ADD_MAKEFILE_FRAGMENT();
1314
}
+317
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
/*
2+
+----------------------------------------------------------------------+
3+
| Copyright (c) The PHP Group |
4+
+----------------------------------------------------------------------+
5+
| This source file is subject to version 3.01 of the PHP license, |
6+
| that is bundled with this package in the file LICENSE, and is |
7+
| available through the world-wide-web at the following url: |
8+
| https://www.php.net/license/3_01.txt |
9+
| If you did not receive a copy of the PHP license and are unable to |
10+
| obtain it through the world-wide-web, please send a note to |
11+
| license@php.net so we can mail you a copy immediately. |
12+
+----------------------------------------------------------------------+
13+
| Author: Christoph M. Becker <cmb@php.net> |
14+
| Based on: <https://thrysoee.dk/InsideCOM+/> |
15+
+----------------------------------------------------------------------+
16+
*/
17+
18+
#include "comtest.h" // Generated by MIDL
19+
20+
long g_cComponents = 0;
21+
long g_cLocks = 0;
22+
23+
class CDocument : public IDocument, IPersistStream
24+
{
25+
public:
26+
// IUnknown
27+
STDMETHODIMP_(ULONG) AddRef();
28+
STDMETHODIMP_(ULONG) Release();
29+
STDMETHODIMP QueryInterface(REFIID riid, void** ppv);
30+
31+
// IDispatch
32+
STDMETHODIMP GetTypeInfoCount(UINT* pCountTypeInfo);
33+
STDMETHODIMP GetTypeInfo(UINT iTypeInfo, LCID lcid, ITypeInfo** ppITypeInfo);
34+
STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgDispId);
35+
STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams,
36+
VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr);
37+
38+
// IDocument
39+
STDMETHODIMP get_Content(BSTR* retvalue);
40+
STDMETHODIMP put_Content(BSTR newvalue);
41+
42+
// IPersist
43+
STDMETHODIMP GetClassID(CLSID *pClassID);
44+
45+
// IPersistStream
46+
STDMETHODIMP IsDirty( void);
47+
STDMETHODIMP Load(IStream *pStm);
48+
STDMETHODIMP Save(IStream *pStm, BOOL fClearDirty);
49+
STDMETHODIMP GetSizeMax(ULARGE_INTEGER *pcbSize);
50+
51+
CDocument() : m_content(SysAllocString(L"")), m_cRef(1) { g_cComponents++; }
52+
~CDocument() { SysFreeString(m_content); g_cComponents--; }
53+
HRESULT Init(void);
54+
55+
private:
56+
BSTR m_content;
57+
ULONG m_cRef;
58+
ITypeInfo* m_pTypeInfo;
59+
BOOL m_dirty;
60+
};
61+
62+
ULONG CDocument::AddRef()
63+
{
64+
return ++m_cRef;
65+
}
66+
67+
ULONG CDocument::Release()
68+
{
69+
if (--m_cRef != 0) {
70+
return m_cRef;
71+
}
72+
m_pTypeInfo->Release();
73+
delete this;
74+
return 0;
75+
}
76+
77+
HRESULT CDocument::QueryInterface(REFIID riid, void** ppv)
78+
{
79+
if (riid == IID_IUnknown) {
80+
*ppv = static_cast<IDocument*>(this);
81+
} else if (riid == IID_IDocument) {
82+
*ppv = static_cast<IDocument*>(this);
83+
} else if (riid == IID_IDispatch) {
84+
*ppv = static_cast<IDispatch*>(this);
85+
} else if (riid == IID_IPersistStream) {
86+
*ppv = static_cast<IPersistStream*>(this);
87+
} else {
88+
*ppv = NULL;
89+
return E_NOINTERFACE;
90+
}
91+
AddRef();
92+
return S_OK;
93+
}
94+
95+
HRESULT CDocument::GetTypeInfoCount(UINT* pCountTypeInfo)
96+
{
97+
*pCountTypeInfo = 1;
98+
return S_OK;
99+
}
100+
101+
HRESULT CDocument::GetTypeInfo(UINT iTypeInfo, LCID lcid, ITypeInfo** ppITypeInfo)
102+
{
103+
(void) lcid;
104+
*ppITypeInfo = NULL;
105+
if (iTypeInfo != 0) {
106+
return DISP_E_BADINDEX;
107+
}
108+
m_pTypeInfo->AddRef();
109+
*ppITypeInfo = m_pTypeInfo;
110+
return S_OK;
111+
}
112+
113+
HRESULT CDocument::GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgDispId)
114+
{
115+
(void) lcid;
116+
if (riid != IID_NULL) {
117+
return DISP_E_UNKNOWNINTERFACE;
118+
}
119+
return DispGetIDsOfNames(m_pTypeInfo, rgszNames, cNames, rgDispId);
120+
}
121+
122+
HRESULT CDocument::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams,
123+
VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr)
124+
{
125+
(void) lcid;
126+
if (riid != IID_NULL) {
127+
return DISP_E_UNKNOWNINTERFACE;
128+
}
129+
return DispInvoke(this, m_pTypeInfo, dispIdMember, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr);
130+
}
131+
132+
HRESULT CDocument::get_Content(BSTR* retvalue)
133+
{
134+
*retvalue = SysAllocString(m_content);
135+
return S_OK;
136+
}
137+
138+
HRESULT CDocument::put_Content(BSTR newvalue)
139+
{
140+
SysFreeString(m_content);
141+
m_content = SysAllocString(newvalue);
142+
return S_OK;
143+
}
144+
145+
HRESULT CDocument::Init(void)
146+
{
147+
ITypeLib* pTypeLib;
148+
if (FAILED(LoadRegTypeLib(LIBID_ComTest, 1, 0, LANG_NEUTRAL, &pTypeLib))) {
149+
return E_FAIL;
150+
}
151+
HRESULT hr = pTypeLib->GetTypeInfoOfGuid(IID_IDocument, &m_pTypeInfo);
152+
if (FAILED(hr)) {
153+
pTypeLib->Release();
154+
return hr;
155+
}
156+
157+
pTypeLib->Release();
158+
return S_OK;
159+
}
160+
161+
HRESULT CDocument::GetClassID(CLSID *pClassID)
162+
{
163+
*pClassID = CLSID_Document;
164+
return S_OK;
165+
}
166+
167+
HRESULT CDocument::IsDirty(void)
168+
{
169+
return m_dirty ? S_OK : S_FALSE;
170+
}
171+
172+
HRESULT CDocument::Load(IStream *pStm)
173+
{
174+
ULONG read = 0;
175+
ULONG cbSize;
176+
177+
if (FAILED(pStm->Read(&cbSize, sizeof cbSize, &read))) {
178+
return S_FALSE;
179+
}
180+
if (!SysReAllocStringLen(&m_content, NULL, cbSize)) {
181+
return S_FALSE;
182+
}
183+
if (FAILED(pStm->Read(m_content, cbSize, &read))) {
184+
// we may have garbage in m_content, but don't mind
185+
return S_FALSE;
186+
}
187+
m_dirty = FALSE;
188+
return S_OK;
189+
}
190+
191+
HRESULT CDocument::Save(IStream *pStm, BOOL fClearDirty)
192+
{
193+
ULONG written = 0;
194+
ULONG cbSize = SysStringByteLen(m_content);
195+
HRESULT hr;
196+
if (FAILED(hr = pStm->Write(&cbSize, sizeof cbSize, &written))) {
197+
return S_FALSE;
198+
}
199+
if (FAILED(hr = pStm->Write(m_content, cbSize, &written))) {
200+
return S_FALSE;
201+
}
202+
203+
if (fClearDirty) {
204+
m_dirty = FALSE;
205+
}
206+
207+
return S_OK;
208+
}
209+
210+
HRESULT CDocument::GetSizeMax(ULARGE_INTEGER *pcbSize)
211+
{
212+
(*pcbSize).QuadPart = sizeof(ULONG) + SysStringByteLen(m_content);
213+
return S_OK;
214+
}
215+
216+
class CDocumentFactory : public IClassFactory
217+
{
218+
public:
219+
// IUnknown
220+
STDMETHODIMP_(ULONG) AddRef();
221+
STDMETHODIMP_(ULONG) Release();
222+
STDMETHODIMP QueryInterface(REFIID riid, void** ppv);
223+
224+
// IClassFactory
225+
STDMETHODIMP CreateInstance(IUnknown* pUnknownOuter, REFIID riid, void** ppv);
226+
STDMETHODIMP LockServer(BOOL bLock);
227+
228+
CDocumentFactory() : m_cRef(1) { g_cLocks++; }
229+
~CDocumentFactory() { g_cLocks--; }
230+
231+
private:
232+
ULONG m_cRef;
233+
};
234+
235+
ULONG CDocumentFactory::AddRef()
236+
{
237+
return ++m_cRef;
238+
}
239+
240+
ULONG CDocumentFactory::Release()
241+
{
242+
if (--m_cRef != 0) {
243+
return m_cRef;
244+
}
245+
delete this;
246+
return 0;
247+
}
248+
249+
HRESULT CDocumentFactory::QueryInterface(REFIID riid, void** ppv)
250+
{
251+
if (riid == IID_IUnknown) {
252+
*ppv = (IUnknown*)this;
253+
} else if (riid == IID_IClassFactory) {
254+
*ppv = (IClassFactory*)this;
255+
} else {
256+
*ppv = NULL;
257+
return E_NOINTERFACE;
258+
}
259+
AddRef();
260+
return S_OK;
261+
}
262+
263+
HRESULT CDocumentFactory::CreateInstance(IUnknown *pUnknownOuter, REFIID riid, void** ppv)
264+
{
265+
if (pUnknownOuter != NULL) {
266+
return CLASS_E_NOAGGREGATION;
267+
}
268+
269+
CDocument *pDocument = new CDocument;
270+
if (pDocument == NULL) {
271+
return E_OUTOFMEMORY;
272+
}
273+
274+
HRESULT hr;
275+
if (FAILED(hr = pDocument->Init())) {
276+
return hr;
277+
}
278+
hr = pDocument->QueryInterface(riid, ppv);
279+
pDocument->Release();
280+
return hr;
281+
}
282+
283+
HRESULT CDocumentFactory::LockServer(BOOL bLock)
284+
{
285+
if (bLock) {
286+
g_cLocks++;
287+
} else {
288+
g_cLocks --;
289+
}
290+
return S_OK;
291+
}
292+
293+
HRESULT __stdcall DllCanUnloadNow()
294+
{
295+
if (g_cLocks == 0) {
296+
return S_OK;
297+
} else {
298+
return S_FALSE;
299+
}
300+
}
301+
302+
HRESULT __stdcall DllGetClassObject(REFCLSID clsid, REFIID riid, void** ppv)
303+
{
304+
if (clsid != CLSID_Document) {
305+
return CLASS_E_CLASSNOTAVAILABLE;
306+
}
307+
308+
CDocumentFactory* pFactory = new CDocumentFactory;
309+
if (pFactory == NULL) {
310+
return E_OUTOFMEMORY;
311+
}
312+
313+
// riid is probably IID_IClassFactory.
314+
HRESULT hr = pFactory->QueryInterface(riid, ppv);
315+
pFactory->Release();
316+
return hr;
317+
}
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
LIBRARY comtest.dll
2+
EXPORTS
3+
DllGetClassObject PRIVATE
4+
DllCanUnloadNow PRIVATE

0 commit comments

Comments
 (0)