fix(fapi): fix memory leak in FAPI and tests - #695
Merged
Conversation
hyperfinitism
force-pushed
the
fix/fapi-decrypt-leak
branch
from
July 6, 2026 18:48
f6df147 to
b707919
Compare
hyperfinitism
force-pushed
the
fix/fapi-decrypt-leak
branch
3 times, most recently
from
July 7, 2026 02:41
9884b3e to
6ffc3c8
Compare
whooo
requested changes
Jul 8, 2026
Comment on lines
-127
to
+140
| p = policy(polstr, TPM2_ALG.SHA256) | ||
| p.set_callback(policy_cb_types.CALC_PCR, test) | ||
| cb = p._get_callback(policy_cb_types.CALC_PCR) | ||
| self.assertEqual(cb, test) | ||
| with policy(polstr, TPM2_ALG.SHA256) as p: | ||
| p.set_callback(policy_cb_types.CALC_PCR, test) | ||
| cb = p._get_callback(policy_cb_types.CALC_PCR) | ||
| self.assertEqual(cb, test) | ||
|
|
||
| p.set_callback(policy_cb_types.CALC_PCR, None) | ||
| cb = p._get_callback(policy_cb_types.CALC_PCR) | ||
| self.assertEqual(cb, None) | ||
| p.set_callback(policy_cb_types.CALC_PCR, None) | ||
| cb = p._get_callback(policy_cb_types.CALC_PCR) | ||
| self.assertEqual(cb, None) | ||
|
|
||
| with self.assertRaises(ValueError) as e: | ||
| p.set_callback(1234, test) | ||
| self.assertEqual(str(e.exception), "unsupported callback type 1234") | ||
| with self.assertRaises(ValueError) as e: | ||
| p.set_callback(1234, test) | ||
| self.assertEqual(str(e.exception), "unsupported callback type 1234") |
Contributor
There was a problem hiding this comment.
I would like to see this in a separate commit (can be the same PR)
Contributor
Author
There was a problem hiding this comment.
I have split it into two commits.
Comment on lines
+121
to
+122
| with MyTCTI(self.tcti) as t: | ||
| with ESAPI(t) as e: |
Contributor
There was a problem hiding this comment.
No need for two lines, both contexts can be on the same block-level
Comment on lines
+211
to
+212
| with MyTCTI(self.tcti) as t: | ||
| e = ESAPI(t) | ||
| r = e.get_random(4) | ||
| self.assertEqual(len(r), 4) | ||
| e.startup(TPM2_SU.CLEAR) | ||
| with ESAPI(t) as e: |
Contributor
There was a problem hiding this comment.
Same here, move both contexts to the same level
Comment on lines
-121
to
217
| t = MyTCTI(self.tcti) | ||
| e = ESAPI(t) | ||
| e.get_random(4) | ||
| with MyTCTI(self.tcti) as t: | ||
| with ESAPI(t) as e: | ||
| e.get_random(4) | ||
|
|
||
| e.startup(TPM2_SU.CLEAR) | ||
| e.startup(TPM2_SU.CLEAR) | ||
|
|
||
| def test_custom_pytcti_C_wrapper_transmit_receive(self): | ||
|
|
||
| t = MyTCTI(self.tcti) | ||
|
|
||
| # Go through the C API directly and call transmit and recv | ||
| t.transmit(b"\x80\x01\x00\x00\x00\x0C\x00\x00\x01\x44\x00\x00") | ||
| resp = t.receive(-1) | ||
| self.assertEqual(resp, b"\x80\x01\x00\x00\x00\n\x00\x00\x01\x00") | ||
|
|
||
| def test_custom_pytcti_cancel(self): | ||
| if getattr(self.tcti, "name", "") == "swtpm": | ||
| self.skipTest("cancel not supported by swtpm") | ||
|
|
||
| t = MyTCTI(self.tcti) | ||
|
|
||
| t.transmit(b"\x80\x01\x00\x00\x00\x0C\x00\x00\x01\x44\x00\x00") | ||
| t.cancel() | ||
|
|
||
| def test_custom_pytcti_finalize(self): | ||
| t = MyTCTI(self.tcti) | ||
| t.finalize() | ||
| self.assertTrue(t.is_finalized) | ||
|
|
||
| def test_custom_pytcti_get_poll_handles(self): | ||
| tcti_name = getattr(self.tcti, "name", "") | ||
| t = MyTCTI(self.tcti) | ||
| try: | ||
| handles = t.get_poll_handles() | ||
| for h in handles: | ||
| self.assertTrue(isinstance(h, PollData)) | ||
| except TSS2_Exception as e: | ||
| if e.rc != lib.TSS2_TCTI_RC_NOT_IMPLEMENTED: | ||
| raise e | ||
| else: | ||
| self.skipTest(f"get_poll_handles not supported by {tcti_name}") | ||
|
|
||
| def test_custom_pytcti_set_locality(self): | ||
| t = MyTCTI(self.tcti) | ||
| t.set_locality(TPMA_LOCALITY.TWO) | ||
|
|
||
| def test_custom_pytcti_make_sticky(self): | ||
| t = MyTCTI(None) | ||
| t._error = None | ||
| t.make_sticky(0, 0) | ||
| t.make_sticky(0, 1) | ||
| t.make_sticky(0, False) | ||
|
|
||
| # Test that throwing an exception shows the originating exception | ||
| t._error = RuntimeError("Bills Error") | ||
| with self.assertRaises(RuntimeError, msg="Bills Error"): | ||
| t.make_sticky(5, True) | ||
|
|
||
| t._v2 = None | ||
| with self.assertRaises(TSS2_Exception): | ||
| t.make_sticky(0, 0) | ||
|
|
||
| def test_custom_pytcti_version(self): | ||
| t = MyTCTI(None) | ||
| self.assertEqual(t.version, 2) | ||
|
|
||
| def test_custom_pytcti_magic(self): | ||
| t = MyTCTI(None) | ||
| magic = b"PYTCTI\x00\x00" | ||
| self.assertEqual(t.magic, magic) | ||
|
|
||
| # max magic len | ||
| magic = b"THISISIT" | ||
| t = MyTCTI(None, magic) | ||
| self.assertEqual(t.magic, magic) | ||
|
|
||
| # small magic len | ||
| magic = b"COOL" | ||
| t = MyTCTI(None, magic) | ||
| self.assertEqual(t.magic, magic) | ||
|
|
||
| # min magic | ||
| magic = b"" | ||
| t = MyTCTI(None, magic) | ||
| self.assertEqual(t.magic, magic) | ||
|
|
||
| with self.assertRaises(ValueError): | ||
| MyTCTI(None, b"THISISTOOBIG") | ||
|
|
||
| def test_custom_pytcti_ctx_manager_finalize(self): | ||
| with MyTCTI(self.tcti) as t: | ||
| e = ESAPI(t) | ||
| r = e.get_random(4) | ||
| self.assertEqual(len(r), 4) | ||
| e.startup(TPM2_SU.CLEAR) | ||
| with ESAPI(t) as e: | ||
| r = e.get_random(4) | ||
| self.assertEqual(len(r), 4) | ||
| e.startup(TPM2_SU.CLEAR) | ||
|
|
||
| self.assertTrue(t.is_finalized) |
Contributor
There was a problem hiding this comment.
Please move this to another commit (can be the same as for the policy tests)
Signed-off-by: Takuma IMAMURA <209989118+hyperfinitism@users.noreply.github.com>
hyperfinitism
force-pushed
the
fix/fapi-decrypt-leak
branch
2 times, most recently
from
July 8, 2026 11:57
154b428 to
4155a26
Compare
Signed-off-by: Takuma IMAMURA <209989118+hyperfinitism@users.noreply.github.com>
hyperfinitism
force-pushed
the
fix/fapi-decrypt-leak
branch
from
July 8, 2026 12:04
4155a26 to
e8a99cd
Compare
whooo
approved these changes
Jul 8, 2026
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.
This pull request fixes native memory leaks in FAPI and tests:
decrypt()get_platform_certificate()get_tpm_blobs())test_policy.pytest_callbacks()test_tcti.pytest_custom_pytcti_esapi()test_custom_pytcti_ctx_manager_finalize()For example,
Fapi_Decrypt()returns the plaintext through a FAPI-allocated output buffer. The Python wrapper copied that buffer intobytes, but did not release the native allocation withFapi_Free(). This caused every successfulFAPI.decrypt()call to leak the decrypted plaintext buffer. This PR wraps the returned plaintext pointer with_get_dptr(..., lib.Fapi_Free)before unpacking it, matching the ownership handling already used byFAPI.encrypt()and other wrappers that receive FAPI-allocated output buffers.Reproduction (for FAPI_Decrypt)
Code
run_fapi_decrypt_valgrind.shfapi_decrypt_leak_probe.pyDownload both files from the Gist into the
tpm2-pytssrepository root, then run the shell script from the repository root in an environment withtpm2-pytss,swtpm,tss2-fapi,libtss2-tcti-swtpm, andvalgrindinstalled.Valgrind result before the fix
With
ITERATIONS=200 SIZE=128, Valgrind reported an iteration-scaled definite leak fromFapi_Decrypt_Finish:The
25,600 bytes in 200 blocksleak matches200decrypt operations of a128byte plaintext.Overall leak summary before this fix:
Validation after the fix
The existing FAPI encrypt/decrypt test passes:
After applying this fix and rerunning the shell script, the
Fapi_Decrypt_Finishleak record is gone. The definite leak total drops by exactly25,600bytes and200blocks forITERATIONS=200 SIZE=128, which confirms that the per-decrypt plaintext buffer is now released.Before:
After:
Follow-up proposal: add Valgrind job in CI
Existing tests cannot detect this type of errors. A Valgrind job in CI would help catch similar ownership bugs in CFFI wrappers. (Note added on Jul 7: PR #696 implements this CI job.)