Repository navigation
Return a copy of a weighted value from a list's get, so a chained read does not stop PHP - #112
Merged
Merged
Conversation
Generated by ip-intelligence/on-premise/generate-wrapper.sh from the new ipi-php.i. Only the five weighted list get functions change, each now returning an owned copy.
This was referenced Nov 14, 2026
Merged
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reading a weighted value through a chain of calls, such as
$results->getValuesAsWeightedStringList('RegisteredCountry')->getValue()->get(0)->getValue(), stopped PHP withstd::bad_allocorstd::length_errorand exit code 134, where holding each step in a variable worked. This makes a weighted list'sgetreturn a copy of the weighted value that PHP owns, so the chained read is the held read.Closes #92.
Why it stopped PHP
The C++
Value<T>::getValue()returns its value by value (Value.hppline 131 at 7992af70,T getValue() const), so$w->getValue()is a new list that PHP holds only while the chain needs it. SWIG's PHP vectorgetreturns a reference into the list (Lib/php/std_vector.iline 45 at SWIG v4.5.1,const_reference get(int i)), and the wrapper hands that reference to PHP without ownership. In the generated wrapper atrc36b71fd,PHP_METHOD(FiftyOneDegreesIpiWeightedStringListSwig,get)takes the address of the element (line 14884) and passes it on with ownership 0 (line 14921,SWIG_SetPointerZval(return_value, (void *)result, SWIGTYPE_p_WeightedValueT_std__string_t, 0)). Oncegetreturns, nothing holds the list, PHP frees it, and the weighted value read next points into freed memory. The same holds for the four other weighted list types.Valgrind shows it. With the chained read added to a script and run on
rc's wrapper under Valgrind, run 43588217293 reports "Invalid read of size 8" inFiftyOneDegreesIpiWeightedValueStringSwig::getValueat an address "8 bytes inside a block of size 40 free'd", the block freed bySWIG_Php_free_obj_WeightedStringListSwig, reached from the list value'sgetValue. That run belongs to the check proposed for #94, whose red is this read.No specification covers this, because it is how the PHP binding holds memory, not how a Property behaves. The behaviour the test asks for is that a chained read gives what the same read with each step held in a variable gives.
What changed
Files are linked at the head, 4497a2e0.
ip-intelligence/on-premise/ipi-php.igives each of the five weighted value types (std::string,std::vector<uint8_t>,bool,intanddouble) an output typemap forconst WeightedValue<T> &that returnsnew WeightedValue<T>(*$1)with ownership 1, so PHP owns a copy and frees it with the object. Only the lists'getreturns that type by reference, so nothing else in the wrapper changes.ip-intelligence/on-premise/src/php8/ipi_wrap.cxxis regenerated with SWIG 4.5.1 bygenerate-wrapper.sh, the version the committed wrapper was made with. The diff is the fivegetmethods, 15 lines.The test,
ip-intelligence/tests/ChainedCallTests.php, in the Integration suite, runs a script in its own PHP, because reading freed memory can stop PHP. On the free Lite file it readsRegisteredCountry,RegisteredNameandRegisteredOwner1,000 times each, holding each step, then chained, then with only the weighted value kept while other memory is used, and fails unless all three agree every time and PHP ends with status 0.Runs
Each throwaway run uses a workflow with two parts. One job builds SWIG 4.5.1, regenerates the wrapper from
ipi-php.i, and compares it with the committed wrapper. The test jobs build both extensions as CI does, put the free Lite IP Intelligence file wherephp.ininames it, and runChainedCallTestsandIpIntelligenceOnPremiseTestswith PHP 8.0, 8.3 and 8.5.rc2104ad7. With PHP 8.0, 8.3 and 8.5 the chained call test failed ("Tests: 20, Assertions: 123, Failures: 1"), PHP ending withstd::bad_allocorstd::length_errorand status 134, and the other 19 tests passed.rc36b71fd was merged in at 976da64 with no conflict. It had changedipi-php.iand the wrapper in another place (#100).ipi-php.iand found it equal to the committed one (git diff --exit-code), and with PHP 8.0, 8.3 and 8.5 the 20 tests passed, the chained call test included ("OK (20 tests, 127 assertions)").rc49ea15c, which holds #117, was merged in at 4497a2e with no conflict. Build with GCC 15 by taking the cxx rc that fixes it, and remove each on-premise extension's settings when it shuts down so dl() no longer corrupts the heap #117 moved thecxxsubmodule to 0312236c and changedipi-php.iand the wrapper in another place (its shutdown now removes the extension's settings), and the mergedipi-php.ikeeps both changes. The generator run, run 43661338232 on a97b7a7, whose parent is 4497a2e, regenerated the wrapper with SWIG 4.5.1 from the mergedipi-php.iandcxx0312236c. It came out the same, byte for byte, as the wrapper the merge made, and bothgenerate-wrapper.sh --checkruns passed, so no wrapper commit was needed. The draft's own wrapper checks passed on 4497a2e (run 43662236020).Setting 'PullRequestSha'lines on 4497a2e, and Configure printed "Left out 5 macOS entries". It ran onrc49ea15c, already merged into the head, which holds #117 and its move ofcxxto 0312236c. All eight jobs passed, Ubuntu with PHP 7.4 and 8.0 to 8.5 and Windows with PHP 8.3. In the Ubuntu PHP 8.3 job, on ubuntu-26.04, the Unit suites passed 1,044 tests (pipeline/core 509 with 1 skipped, pipeline/engines 9, pipeline/cloudrequestengine 90, pipeline/did 192, pipeline/translation 22, pipeline/derivedproperty 52, device-detection/cloud 29, device-detection/onpremise 89 and ip-intelligence 52) and the Integration suites 133 (pipeline/cloudrequestengine 6, device-detection/cloud 36, location 16 and ip-intelligence 75).macOS was left out of this draft's full build under the 9 November decision. The rc head is built with macOS before it merges into main.
What remains
getcopies into PHP strings, and its three results handed to PHP without ownership (getMetaData,getKeysandengine_getinhash_wrap.cxx) point into the engine, which lives as long as PHP.getMetaData,getKeysandipi_engine_get. The configuration's eleven collection getters (getStrings,getGraphand the others onFiftyOneDegreesIpiConfigIpiSwig) also hand PHP a pointer into the configuration without ownership. That is safe while the configuration is held, as the engine builder and every example hold it, and a chain from a configuration that nothing holds would meet the same fault. Nothing reads them that way, and this draft leaves them. Raised as #147.