Fix crash in supautils_executor_start#190
Conversation
supautils_executor_start makes some assumptions about the ERROR it is intercepting. Following are addressed: - Fixed cases where the target object isn't a relation - Fixed possible cases of trying to free a NULL variable - Fixed possible leak of an old hint (if there were one)
Coverage Report for CI Build 25060370145Coverage increased (+0.05%) to 88.79%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
There was a problem hiding this comment.
Pull request overview
This PR adjusts supautils_executor_start’s permission-hint enrichment to be safer when constructing edata->hint, aiming to avoid crashes and memory issues while intercepting ERRCODE_INSUFFICIENT_PRIVILEGE.
Changes:
- Adds a NULL check around relation-name handling before composing a GRANT hint.
- Frees any existing
edata->hintbefore replacing it to avoid leaking the old hint. - Adds/adjusts
pfreecleanup paths to avoid freeing NULL and to release intermediate strings.
| char *schema = get_namespace_name(get_rel_namespace(missing.relid)); | ||
| char *relname = get_rel_name(missing.relid); | ||
| char *qualified_rel_name = quote_qualified_identifier(schema, relname); | ||
| char *quoted_role_name = quote_qualified_identifier( | ||
| NULL, GetUserNameFromId(current_role_oid, false)); | ||
|
|
||
| edata->hint = psprintf("Grant the required privileges to the current " | ||
| "role with: GRANT %s ON %s TO %s;", | ||
| privileges_str->data, qualified_rel_name, | ||
| quoted_role_name); | ||
| if (relname != NULL) { | ||
| char *qualified_rel_name = |
This comment was marked as low quality.
This comment was marked as low quality.
Sorry, something went wrong.
Indeed asserts are not a proper way to protect against InvalidOid or 0 ACLs. Use proper runtime guards instead.
| pfree(username); | ||
| pfree(qualified_rel_name); | ||
| pfree(quoted_role_name); |
This comment was marked as low quality.
This comment was marked as low quality.
Sorry, something went wrong.
| if (edata->sqlerrcode == ERRCODE_INSUFFICIENT_PRIVILEGE) { | ||
| const Oid current_role_oid = GetUserId(); | ||
|
|
||
| // edata->table_name is always NULL for some reason, so we need to find | ||
| // the relid (included in missing_perm) | ||
| missing_perm missing = | ||
| find_missing_perm(queryDesc->plannedstmt, current_role_oid); | ||
|
|
||
| // there must be some privilege missing given there was a | ||
| // ERRCODE_INSUFFICIENT_PRIVILEGE | ||
| Assert(missing.acl != 0); | ||
| Assert(missing.relid != InvalidOid); | ||
|
|
||
| // ERRCODE_INSUFFICIENT_PRIVILEGE also includes TRUNCATE, REFERENCES and | ||
| // TRIGGER error privileges but these are not visible in this function | ||
| // (a ExecutorStart_hook) so we assert here that these are impossible as | ||
| // a missing privilege | ||
| Assert((missing.acl & (ACL_TRUNCATE | ACL_TRIGGER | ACL_REFERENCES)) == | ||
| 0); | ||
|
|
||
| StringInfo privileges_str = makeStringInfo(); | ||
| build_privileges_string(privileges_str, missing.acl); | ||
|
|
||
| // the string of privileges has to be built | ||
| Assert(privileges_str->len > 0); | ||
|
|
||
| char *schema = get_namespace_name(get_rel_namespace(missing.relid)); | ||
| char *relname = get_rel_name(missing.relid); | ||
| char *qualified_rel_name = quote_qualified_identifier(schema, relname); | ||
| char *quoted_role_name = quote_qualified_identifier( | ||
| NULL, GetUserNameFromId(current_role_oid, false)); | ||
|
|
||
| edata->hint = psprintf("Grant the required privileges to the current " | ||
| "role with: GRANT %s ON %s TO %s;", | ||
| privileges_str->data, qualified_rel_name, | ||
| quoted_role_name); | ||
|
|
||
| destroyStringInfo(privileges_str); | ||
| pfree(schema); | ||
| pfree(relname); | ||
| pfree(qualified_rel_name); | ||
| pfree(quoted_role_name); | ||
| if (missing.acl != 0 && OidIsValid(missing.relid) && | ||
| (missing.acl & (ACL_TRUNCATE | ACL_TRIGGER | ACL_REFERENCES)) == | ||
| 0) { |
There was a problem hiding this comment.
This change is intended to avoid crashes when ERRCODE_INSUFFICIENT_PRIVILEGE is raised for non-relation objects (schemas/functions/etc). There are existing regression tests for permission hints (test/sql/permission_hints.sql); please add coverage for at least one non-relation insufficient-privilege error to ensure the hook no longer asserts/crashes and to pin the expected hint behavior (likely: no enhanced hint).
| pfree(relname); | ||
| pfree(qualified_rel_name); | ||
| pfree(quoted_role_name); | ||
| if (missing.acl != 0 && OidIsValid(missing.relid) && |
There was a problem hiding this comment.
| if (missing.acl != 0 && OidIsValid(missing.relid) && | |
| if (missing.acl != ACL_NO_RIGHTS && OidIsValid(missing.relid) && |
|
|
||
| if (privileges_str->len > 0) { | ||
| char *schema = get_namespace_name(get_rel_namespace(missing.relid)); | ||
| char *relname = get_rel_name(missing.relid); |
There was a problem hiding this comment.
Can get_rel_name return NULL here if missing.relid is not InvalidOid? We check missing.relid above and if get_rel_name returns NULL then this is usually a cache error.
There was a problem hiding this comment.
Good point. Now that we don't simply rely on asserts to check if relid is 0 it might be unnecessary. Before we did end up here with relid=0 and that was the main cause for the crash.
There was a problem hiding this comment.
We should do this refactor now otherwise it will be harder to change later
There was a problem hiding this comment.
if get_rel_name returns NULL then this is usually a cache error.
On second though, if what Artur mentions above is true, then maybe we should leave it as extra layer of defense.
| pfree(username); | ||
| pfree(qualified_rel_name); | ||
| pfree(quoted_role_name); |
Yes, it was limited on purpose and it doesn't work for views too #182. Will put a note there.
Where would |
Signed-off-by: Bobbie Soedirgo <bobbie@soedirgo.dev>
Signed-off-by: Bobbie Soedirgo <bobbie@soedirgo.dev>
That's the point: the user would get an error and then call the function manually. No need to hook into anything. That's just asking for problems at this level. IMO error handling is a critical enough path that we should be very careful what we change there. |
@pgnickb When I looked at that option there was no way to obtain which exact privileges were missing (this is lost after the executor stage) and this was a requirement. |
supautils_executor_startmakes some assumptions about theERRORit is intercepting. Following are addressed:NULLvariableThis fixes existing bugs. However it doesn't add the missing functionality for the cases when privileges are missing for functions, schemas etc.
I also want to point out that executor is a very low level API and hooking into it merely to slightly improve user experience seems like an overkill to me. We could consider writing a standalone function to the effect of
check_permissions(objclassid, objid, rolename)that would perform the same checks as the hook, but won't reside in a critical codepath so any potential bugs won't crash the database.